mirror of
https://github.com/openai/codex.git
synced 2026-10-24 18:18:41 +00:00
Merge remote-tracking branch 'origin/main' into dev/abhinav/permission-request-hook
# Conflicts: # codex-rs/core/src/guardian/review.rs # codex-rs/core/src/tools/orchestrator.rs # codex-rs/core/src/tools/runtimes/shell.rs # codex-rs/core/src/tools/runtimes/unified_exec.rs
This commit is contained in:
Generated
+1
-11
@@ -1366,16 +1366,6 @@ dependencies = [
|
||||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-account"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"codex-backend-client",
|
||||
"codex-login",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-analytics"
|
||||
version = "0.0.0"
|
||||
@@ -1908,7 +1898,6 @@ dependencies = [
|
||||
"bm25",
|
||||
"chrono",
|
||||
"clap",
|
||||
"codex-account",
|
||||
"codex-analytics",
|
||||
"codex-api",
|
||||
"codex-app-server-protocol",
|
||||
@@ -2942,6 +2931,7 @@ name = "codex-utils-absolute-path"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"dunce",
|
||||
"pretty_assertions",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"account",
|
||||
"analytics",
|
||||
"backend-client",
|
||||
"ansi-escape",
|
||||
@@ -105,7 +104,6 @@ license = "Apache-2.0"
|
||||
# Internal
|
||||
app_test_support = { path = "app-server/tests/common" }
|
||||
codex-analytics = { path = "analytics" }
|
||||
codex-account = { path = "account" }
|
||||
codex-ansi-escape = { path = "ansi-escape" }
|
||||
codex-api = { path = "codex-api" }
|
||||
codex-app-server = { path = "app-server" }
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "account",
|
||||
crate_name = "codex_account",
|
||||
)
|
||||
@@ -1,19 +0,0 @@
|
||||
[package]
|
||||
name = "codex-account"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
name = "codex_account"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
codex-backend-client = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
@@ -1,121 +0,0 @@
|
||||
use codex_backend_client::Client as BackendClient;
|
||||
use codex_backend_client::RequestError;
|
||||
use codex_backend_client::WorkspaceRole as BackendWorkspaceRole;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum WorkspaceRole {
|
||||
AccountOwner,
|
||||
AccountAdmin,
|
||||
StandardUser,
|
||||
}
|
||||
|
||||
impl WorkspaceRole {
|
||||
fn is_workspace_owner(self) -> bool {
|
||||
matches!(self, Self::AccountOwner | Self::AccountAdmin)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BackendWorkspaceRole> for WorkspaceRole {
|
||||
fn from(value: BackendWorkspaceRole) -> Self {
|
||||
match value {
|
||||
BackendWorkspaceRole::AccountOwner => Self::AccountOwner,
|
||||
BackendWorkspaceRole::AccountAdmin => Self::AccountAdmin,
|
||||
BackendWorkspaceRole::StandardUser => Self::StandardUser,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct WorkspaceOwnership {
|
||||
pub workspace_role: Option<WorkspaceRole>,
|
||||
pub is_workspace_owner: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AddCreditsNudgeEmailStatus {
|
||||
Sent,
|
||||
CooldownActive,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SendAddCreditsNudgeEmailError {
|
||||
#[error("codex account authentication required to notify workspace owner")]
|
||||
AuthRequired,
|
||||
|
||||
#[error("chatgpt authentication required to notify workspace owner")]
|
||||
ChatGptAuthRequired,
|
||||
|
||||
#[error("failed to construct backend client: {0}")]
|
||||
CreateClient(#[from] anyhow::Error),
|
||||
|
||||
#[error("failed to notify workspace owner: {0}")]
|
||||
Request(#[from] RequestError),
|
||||
}
|
||||
|
||||
pub async fn send_add_credits_nudge_email(
|
||||
chatgpt_base_url: impl Into<String>,
|
||||
auth_manager: &AuthManager,
|
||||
) -> Result<AddCreditsNudgeEmailStatus, SendAddCreditsNudgeEmailError> {
|
||||
let auth = auth_manager
|
||||
.auth()
|
||||
.await
|
||||
.ok_or(SendAddCreditsNudgeEmailError::AuthRequired)?;
|
||||
send_add_credits_nudge_email_for_auth(chatgpt_base_url, &auth).await
|
||||
}
|
||||
|
||||
pub async fn send_add_credits_nudge_email_for_auth(
|
||||
chatgpt_base_url: impl Into<String>,
|
||||
auth: &CodexAuth,
|
||||
) -> Result<AddCreditsNudgeEmailStatus, SendAddCreditsNudgeEmailError> {
|
||||
if !auth.is_chatgpt_auth() {
|
||||
return Err(SendAddCreditsNudgeEmailError::ChatGptAuthRequired);
|
||||
}
|
||||
|
||||
let client = BackendClient::from_auth(chatgpt_base_url, auth)?;
|
||||
match client.send_add_credits_nudge_email().await {
|
||||
Ok(()) => Ok(AddCreditsNudgeEmailStatus::Sent),
|
||||
Err(err) if err.status().is_some_and(|status| status.as_u16() == 429) => {
|
||||
Ok(AddCreditsNudgeEmailStatus::CooldownActive)
|
||||
}
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn resolve_workspace_role_and_owner_for_auth(
|
||||
chatgpt_base_url: &str,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> WorkspaceOwnership {
|
||||
let token_is_workspace_owner = auth.and_then(CodexAuth::is_workspace_owner);
|
||||
let Some(auth) = auth else {
|
||||
return WorkspaceOwnership::default();
|
||||
};
|
||||
|
||||
let workspace_role = fetch_current_workspace_role_for_auth(chatgpt_base_url, auth).await;
|
||||
let is_workspace_owner = workspace_role
|
||||
.map(WorkspaceRole::is_workspace_owner)
|
||||
.or(token_is_workspace_owner);
|
||||
WorkspaceOwnership {
|
||||
workspace_role,
|
||||
is_workspace_owner,
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_current_workspace_role_for_auth(
|
||||
chatgpt_base_url: &str,
|
||||
auth: &CodexAuth,
|
||||
) -> Option<WorkspaceRole> {
|
||||
if !auth.is_chatgpt_auth() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let client = BackendClient::from_auth(chatgpt_base_url.to_string(), auth).ok()?;
|
||||
client
|
||||
.get_current_workspace_role()
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(WorkspaceRole::from)
|
||||
}
|
||||
@@ -20,6 +20,7 @@ codex-plugin = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
os_info = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
sha1 = { workspace = true }
|
||||
tokio = { workspace = true, features = [
|
||||
"macros",
|
||||
@@ -29,4 +30,3 @@ tracing = { workspace = true, features = ["log"] }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::events::AppServerRpcTransport;
|
||||
use crate::events::GuardianReviewEventParams;
|
||||
use crate::events::TrackEventRequest;
|
||||
use crate::events::TrackEventsRequest;
|
||||
use crate::events::current_runtime_metadata;
|
||||
@@ -151,6 +152,12 @@ impl AnalyticsEventsClient {
|
||||
));
|
||||
}
|
||||
|
||||
pub fn track_guardian_review(&self, input: GuardianReviewEventParams) {
|
||||
self.record_fact(AnalyticsFact::Custom(CustomAnalyticsFact::GuardianReview(
|
||||
Box::new(input),
|
||||
)));
|
||||
}
|
||||
|
||||
pub fn track_app_mentioned(&self, tracking: TrackEventsContext, mentions: Vec<AppInvocation>) {
|
||||
if mentions.is_empty() {
|
||||
return;
|
||||
|
||||
@@ -6,6 +6,9 @@ use crate::facts::SubAgentThreadStartedInput;
|
||||
use crate::facts::TrackEventsContext;
|
||||
use codex_login::default_client::originator;
|
||||
use codex_plugin::PluginTelemetryMetadata;
|
||||
use codex_protocol::approvals::NetworkApprovalProtocol;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::models::SandboxPermissions;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use serde::Serialize;
|
||||
@@ -36,6 +39,7 @@ pub(crate) struct TrackEventsRequest {
|
||||
pub(crate) enum TrackEventRequest {
|
||||
SkillInvocation(SkillInvocationEventRequest),
|
||||
ThreadInitialized(ThreadInitializedEvent),
|
||||
GuardianReview(Box<GuardianReviewEventRequest>),
|
||||
AppMentioned(CodexAppMentionedEventRequest),
|
||||
AppUsed(CodexAppUsedEventRequest),
|
||||
Compaction(Box<CodexCompactionEventRequest>),
|
||||
@@ -101,6 +105,179 @@ pub(crate) struct ThreadInitializedEvent {
|
||||
pub(crate) event_params: ThreadInitializedEventParams,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct GuardianReviewEventRequest {
|
||||
pub(crate) event_type: &'static str,
|
||||
pub(crate) event_params: GuardianReviewEventPayload,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GuardianReviewDecision {
|
||||
Approved,
|
||||
Denied,
|
||||
Aborted,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GuardianReviewTerminalStatus {
|
||||
Approved,
|
||||
Denied,
|
||||
Aborted,
|
||||
TimedOut,
|
||||
FailedClosed,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GuardianReviewFailureReason {
|
||||
Timeout,
|
||||
Cancelled,
|
||||
PromptBuildError,
|
||||
SessionError,
|
||||
ParseError,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GuardianReviewSessionKind {
|
||||
TrunkNew,
|
||||
TrunkReused,
|
||||
EphemeralForked,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum GuardianReviewRiskLevel {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
Critical,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum GuardianReviewUserAuthorization {
|
||||
Unknown,
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum GuardianReviewOutcome {
|
||||
Allow,
|
||||
Deny,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GuardianApprovalRequestSource {
|
||||
/// Approval requested directly by the main Codex turn.
|
||||
MainTurn,
|
||||
/// Approval requested by a delegated subagent and routed through the parent
|
||||
/// session for guardian review.
|
||||
DelegatedSubagent,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum GuardianReviewedAction {
|
||||
Shell {
|
||||
command: Vec<String>,
|
||||
command_display: String,
|
||||
cwd: String,
|
||||
sandbox_permissions: SandboxPermissions,
|
||||
additional_permissions: Option<PermissionProfile>,
|
||||
justification: Option<String>,
|
||||
},
|
||||
UnifiedExec {
|
||||
command: Vec<String>,
|
||||
command_display: String,
|
||||
cwd: String,
|
||||
sandbox_permissions: SandboxPermissions,
|
||||
additional_permissions: Option<PermissionProfile>,
|
||||
justification: Option<String>,
|
||||
tty: bool,
|
||||
},
|
||||
Execve {
|
||||
source: GuardianCommandSource,
|
||||
program: String,
|
||||
argv: Vec<String>,
|
||||
cwd: String,
|
||||
additional_permissions: Option<PermissionProfile>,
|
||||
},
|
||||
ApplyPatch {
|
||||
cwd: String,
|
||||
files: Vec<String>,
|
||||
},
|
||||
NetworkAccess {
|
||||
target: String,
|
||||
host: String,
|
||||
protocol: NetworkApprovalProtocol,
|
||||
port: u16,
|
||||
},
|
||||
McpToolCall {
|
||||
server: String,
|
||||
tool_name: String,
|
||||
connector_id: Option<String>,
|
||||
connector_name: Option<String>,
|
||||
tool_title: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GuardianCommandSource {
|
||||
Shell,
|
||||
UnifiedExec,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
pub struct GuardianReviewEventParams {
|
||||
pub thread_id: String,
|
||||
pub turn_id: String,
|
||||
pub review_id: String,
|
||||
pub target_item_id: String,
|
||||
pub retry_reason: Option<String>,
|
||||
pub approval_request_source: GuardianApprovalRequestSource,
|
||||
pub reviewed_action: GuardianReviewedAction,
|
||||
pub reviewed_action_truncated: bool,
|
||||
pub decision: GuardianReviewDecision,
|
||||
pub terminal_status: GuardianReviewTerminalStatus,
|
||||
pub failure_reason: Option<GuardianReviewFailureReason>,
|
||||
pub risk_level: Option<GuardianReviewRiskLevel>,
|
||||
pub user_authorization: Option<GuardianReviewUserAuthorization>,
|
||||
pub outcome: Option<GuardianReviewOutcome>,
|
||||
pub rationale: Option<String>,
|
||||
pub guardian_thread_id: Option<String>,
|
||||
pub guardian_session_kind: Option<GuardianReviewSessionKind>,
|
||||
pub guardian_model: Option<String>,
|
||||
pub guardian_reasoning_effort: Option<String>,
|
||||
pub had_prior_review_context: Option<bool>,
|
||||
pub review_timeout_ms: u64,
|
||||
pub tool_call_count: u64,
|
||||
pub time_to_first_token_ms: Option<u64>,
|
||||
pub completion_latency_ms: Option<u64>,
|
||||
pub started_at: u64,
|
||||
pub completed_at: Option<u64>,
|
||||
pub input_tokens: Option<i64>,
|
||||
pub cached_input_tokens: Option<i64>,
|
||||
pub output_tokens: Option<i64>,
|
||||
pub reasoning_output_tokens: Option<i64>,
|
||||
pub total_tokens: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct GuardianReviewEventPayload {
|
||||
pub(crate) app_server_client: CodexAppServerClientMetadata,
|
||||
pub(crate) runtime: CodexRuntimeMetadata,
|
||||
#[serde(flatten)]
|
||||
pub(crate) guardian_review: GuardianReviewEventParams,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexAppMetadata {
|
||||
pub(crate) connector_id: Option<String>,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::events::AppServerRpcTransport;
|
||||
use crate::events::CodexRuntimeMetadata;
|
||||
use crate::events::GuardianReviewEventParams;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::ClientResponse;
|
||||
use codex_app_server_protocol::InitializeParams;
|
||||
@@ -154,6 +155,7 @@ pub(crate) enum AnalyticsFact {
|
||||
pub(crate) enum CustomAnalyticsFact {
|
||||
SubAgentThreadStarted(SubAgentThreadStartedInput),
|
||||
Compaction(Box<CodexCompactionEvent>),
|
||||
GuardianReview(Box<GuardianReviewEventParams>),
|
||||
SkillInvoked(SkillInvokedInput),
|
||||
AppMentioned(AppMentionedInput),
|
||||
AppUsed(AppUsedInput),
|
||||
|
||||
@@ -5,6 +5,17 @@ mod reducer;
|
||||
|
||||
pub use client::AnalyticsEventsClient;
|
||||
pub use events::AppServerRpcTransport;
|
||||
pub use events::GuardianApprovalRequestSource;
|
||||
pub use events::GuardianCommandSource;
|
||||
pub use events::GuardianReviewDecision;
|
||||
pub use events::GuardianReviewEventParams;
|
||||
pub use events::GuardianReviewFailureReason;
|
||||
pub use events::GuardianReviewOutcome;
|
||||
pub use events::GuardianReviewRiskLevel;
|
||||
pub use events::GuardianReviewSessionKind;
|
||||
pub use events::GuardianReviewTerminalStatus;
|
||||
pub use events::GuardianReviewUserAuthorization;
|
||||
pub use events::GuardianReviewedAction;
|
||||
pub use facts::AppInvocation;
|
||||
pub use facts::CodexCompactionEvent;
|
||||
pub use facts::CompactionImplementation;
|
||||
|
||||
@@ -6,6 +6,9 @@ use crate::events::CodexCompactionEventRequest;
|
||||
use crate::events::CodexPluginEventRequest;
|
||||
use crate::events::CodexPluginUsedEventRequest;
|
||||
use crate::events::CodexRuntimeMetadata;
|
||||
use crate::events::GuardianReviewEventParams;
|
||||
use crate::events::GuardianReviewEventPayload;
|
||||
use crate::events::GuardianReviewEventRequest;
|
||||
use crate::events::SkillInvocationEventParams;
|
||||
use crate::events::SkillInvocationEventRequest;
|
||||
use crate::events::ThreadInitializationMode;
|
||||
@@ -120,6 +123,9 @@ impl AnalyticsReducer {
|
||||
CustomAnalyticsFact::Compaction(input) => {
|
||||
self.ingest_compaction(*input, out);
|
||||
}
|
||||
CustomAnalyticsFact::GuardianReview(input) => {
|
||||
self.ingest_guardian_review(*input, out);
|
||||
}
|
||||
CustomAnalyticsFact::SkillInvoked(input) => {
|
||||
self.ingest_skill_invoked(input, out).await;
|
||||
}
|
||||
@@ -174,6 +180,42 @@ impl AnalyticsReducer {
|
||||
));
|
||||
}
|
||||
|
||||
fn ingest_guardian_review(
|
||||
&mut self,
|
||||
input: GuardianReviewEventParams,
|
||||
out: &mut Vec<TrackEventRequest>,
|
||||
) {
|
||||
let Some(connection_id) = self.thread_connections.get(&input.thread_id) else {
|
||||
tracing::warn!(
|
||||
thread_id = %input.thread_id,
|
||||
turn_id = %input.turn_id,
|
||||
review_id = %input.review_id,
|
||||
"dropping guardian analytics event: missing thread connection metadata"
|
||||
);
|
||||
return;
|
||||
};
|
||||
let Some(connection_state) = self.connections.get(connection_id) else {
|
||||
tracing::warn!(
|
||||
thread_id = %input.thread_id,
|
||||
turn_id = %input.turn_id,
|
||||
review_id = %input.review_id,
|
||||
connection_id,
|
||||
"dropping guardian analytics event: missing connection metadata"
|
||||
);
|
||||
return;
|
||||
};
|
||||
out.push(TrackEventRequest::GuardianReview(Box::new(
|
||||
GuardianReviewEventRequest {
|
||||
event_type: "codex_guardian_review",
|
||||
event_params: GuardianReviewEventPayload {
|
||||
app_server_client: connection_state.app_server_client.clone(),
|
||||
runtime: connection_state.runtime.clone(),
|
||||
guardian_review: input,
|
||||
},
|
||||
},
|
||||
)));
|
||||
}
|
||||
|
||||
async fn ingest_skill_invoked(
|
||||
&mut self,
|
||||
input: SkillInvokedInput,
|
||||
|
||||
@@ -1287,8 +1287,6 @@ mod tests {
|
||||
id: request.id,
|
||||
result: serde_json::to_value(GetAccountResponse {
|
||||
account: None,
|
||||
workspace_role: None,
|
||||
is_workspace_owner: None,
|
||||
requires_openai_auth: false,
|
||||
})
|
||||
.expect("response should serialize"),
|
||||
@@ -1397,8 +1395,6 @@ mod tests {
|
||||
id: request.id,
|
||||
result: serde_json::to_value(GetAccountResponse {
|
||||
account: None,
|
||||
workspace_role: None,
|
||||
is_workspace_owner: None,
|
||||
requires_openai_auth: false,
|
||||
})
|
||||
.expect("response should serialize"),
|
||||
@@ -1452,8 +1448,6 @@ mod tests {
|
||||
first_response,
|
||||
GetAccountResponse {
|
||||
account: None,
|
||||
workspace_role: None,
|
||||
is_workspace_owner: None,
|
||||
requires_openai_auth: false,
|
||||
}
|
||||
);
|
||||
@@ -1473,8 +1467,6 @@ mod tests {
|
||||
AccountUpdatedNotification {
|
||||
auth_mode: None,
|
||||
plan_type: None,
|
||||
workspace_role: None,
|
||||
is_workspace_owner: None,
|
||||
},
|
||||
))
|
||||
.expect("notification should serialize"),
|
||||
|
||||
@@ -2562,17 +2562,6 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadAddCreditsNudgeEmailParams": {
|
||||
"properties": {
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadArchiveParams": {
|
||||
"properties": {
|
||||
"threadId": {
|
||||
@@ -3221,10 +3210,27 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sessionStartSource": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadStartSource"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadStartSource": {
|
||||
"enum": [
|
||||
"startup",
|
||||
"clear"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadUnarchiveParams": {
|
||||
"properties": {
|
||||
"threadId": {
|
||||
@@ -3820,30 +3826,6 @@
|
||||
"title": "Thread/shellCommandRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/addCreditsNudgeEmail"
|
||||
],
|
||||
"title": "Thread/addCreditsNudgeEmailRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadAddCreditsNudgeEmailParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/addCreditsNudgeEmailRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -51,12 +51,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"isWorkspaceOwner": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"planType": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -66,89 +60,10 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"workspaceRole": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/WorkspaceRole"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AddCreditsNudgeEmailNotification": {
|
||||
"properties": {
|
||||
"result": {
|
||||
"$ref": "#/definitions/AddCreditsNudgeEmailResult"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"result",
|
||||
"threadId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AddCreditsNudgeEmailResult": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"status": {
|
||||
"enum": [
|
||||
"sent"
|
||||
],
|
||||
"title": "SentAddCreditsNudgeEmailResultStatus",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status"
|
||||
],
|
||||
"title": "SentAddCreditsNudgeEmailResult",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"status": {
|
||||
"enum": [
|
||||
"cooldownActive"
|
||||
],
|
||||
"title": "CooldownActiveAddCreditsNudgeEmailResultStatus",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status"
|
||||
],
|
||||
"title": "CooldownActiveAddCreditsNudgeEmailResult",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"enum": [
|
||||
"failed"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message",
|
||||
"status"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"AgentMessageDeltaNotification": {
|
||||
"properties": {
|
||||
"delta": {
|
||||
@@ -473,6 +388,13 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"AutoReviewDecisionSource": {
|
||||
"description": "[UNSTABLE] Source that produced a terminal guardian approval review decision.",
|
||||
"enum": [
|
||||
"agent"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -1674,17 +1596,28 @@
|
||||
"type": "object"
|
||||
},
|
||||
"ItemGuardianApprovalReviewCompletedNotification": {
|
||||
"description": "[UNSTABLE] Temporary notification payload for guardian automatic approval review. This shape is expected to change soon.\n\nTODO(ccunningham): Attach guardian review state to the reviewed tool item's lifecycle instead of sending separate standalone review notifications so the app-server API can persist and replay review state via `thread/read`.",
|
||||
"description": "[UNSTABLE] Temporary notification payload for guardian automatic approval review. This shape is expected to change soon.",
|
||||
"properties": {
|
||||
"action": {
|
||||
"$ref": "#/definitions/GuardianApprovalReviewAction"
|
||||
},
|
||||
"decisionSource": {
|
||||
"$ref": "#/definitions/AutoReviewDecisionSource"
|
||||
},
|
||||
"review": {
|
||||
"$ref": "#/definitions/GuardianApprovalReview"
|
||||
},
|
||||
"targetItemId": {
|
||||
"reviewId": {
|
||||
"description": "Stable identifier for this review.",
|
||||
"type": "string"
|
||||
},
|
||||
"targetItemId": {
|
||||
"description": "Identifier for the reviewed item or tool call when one exists.\n\nIn most cases, one review maps to one target item. The exceptions are - execve reviews, where a single command may contain multiple execve calls to review (only possible when using the shell_zsh_fork feature) - network policy reviews, where there is no target item\n\nA network call is triggered by a CommandExecution item, so having a target_item_id set to the CommandExecution item would be misleading because the review is about the network call, not the command execution. Therefore, target_item_id is set to None for network policy reviews.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1694,15 +1627,16 @@
|
||||
},
|
||||
"required": [
|
||||
"action",
|
||||
"decisionSource",
|
||||
"review",
|
||||
"targetItemId",
|
||||
"reviewId",
|
||||
"threadId",
|
||||
"turnId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ItemGuardianApprovalReviewStartedNotification": {
|
||||
"description": "[UNSTABLE] Temporary notification payload for guardian automatic approval review. This shape is expected to change soon.\n\nTODO(ccunningham): Attach guardian review state to the reviewed tool item's lifecycle instead of sending separate standalone review notifications so the app-server API can persist and replay review state via `thread/read`.",
|
||||
"description": "[UNSTABLE] Temporary notification payload for guardian automatic approval review. This shape is expected to change soon.",
|
||||
"properties": {
|
||||
"action": {
|
||||
"$ref": "#/definitions/GuardianApprovalReviewAction"
|
||||
@@ -1710,9 +1644,17 @@
|
||||
"review": {
|
||||
"$ref": "#/definitions/GuardianApprovalReview"
|
||||
},
|
||||
"targetItemId": {
|
||||
"reviewId": {
|
||||
"description": "Stable identifier for this review.",
|
||||
"type": "string"
|
||||
},
|
||||
"targetItemId": {
|
||||
"description": "Identifier for the reviewed item or tool call when one exists.\n\nIn most cases, one review maps to one target item. The exceptions are - execve reviews, where a single command may contain multiple execve calls to review (only possible when using the shell_zsh_fork feature) - network policy reviews, where there is no target item\n\nA network call is triggered by a CommandExecution item, so having a target_item_id set to the CommandExecution item would be misleading because the review is about the network call, not the command execution. Therefore, target_item_id is set to None for network policy reviews.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1723,7 +1665,7 @@
|
||||
"required": [
|
||||
"action",
|
||||
"review",
|
||||
"targetItemId",
|
||||
"reviewId",
|
||||
"threadId",
|
||||
"turnId"
|
||||
],
|
||||
@@ -2131,16 +2073,6 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"spendControl": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SpendControlSnapshot"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -2340,17 +2272,6 @@
|
||||
"description": "Notification emitted when watched local skill files change.\n\nTreat this as an invalidation signal and re-run `skills/list` with the client's current parameters when refreshed skill metadata is needed.",
|
||||
"type": "object"
|
||||
},
|
||||
"SpendControlSnapshot": {
|
||||
"properties": {
|
||||
"reached": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"reached"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SubAgentSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -4135,14 +4056,6 @@
|
||||
"samplePaths"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"WorkspaceRole": {
|
||||
"enum": [
|
||||
"account-owner",
|
||||
"account-admin",
|
||||
"standard-user"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Notification sent from the server to the client.",
|
||||
@@ -4770,26 +4683,6 @@
|
||||
"title": "Account/rateLimits/updatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"account/addCreditsNudgeEmail/completed"
|
||||
],
|
||||
"title": "Account/addCreditsNudgeEmail/completedNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/AddCreditsNudgeEmailNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Account/addCreditsNudgeEmail/completedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
|
||||
@@ -482,30 +482,6 @@
|
||||
"title": "Thread/shellCommandRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/addCreditsNudgeEmail"
|
||||
],
|
||||
"title": "Thread/addCreditsNudgeEmailRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/ThreadAddCreditsNudgeEmailParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/addCreditsNudgeEmailRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -4058,26 +4034,6 @@
|
||||
"title": "Account/rateLimits/updatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"account/addCreditsNudgeEmail/completed"
|
||||
],
|
||||
"title": "Account/addCreditsNudgeEmail/completedNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/AddCreditsNudgeEmailNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Account/addCreditsNudgeEmail/completedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
@@ -4965,12 +4921,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"isWorkspaceOwner": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"planType": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -4980,92 +4930,11 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"workspaceRole": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/WorkspaceRole"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "AccountUpdatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"AddCreditsNudgeEmailNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"result": {
|
||||
"$ref": "#/definitions/v2/AddCreditsNudgeEmailResult"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"result",
|
||||
"threadId"
|
||||
],
|
||||
"title": "AddCreditsNudgeEmailNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"AddCreditsNudgeEmailResult": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"status": {
|
||||
"enum": [
|
||||
"sent"
|
||||
],
|
||||
"title": "SentAddCreditsNudgeEmailResultStatus",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status"
|
||||
],
|
||||
"title": "SentAddCreditsNudgeEmailResult",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"status": {
|
||||
"enum": [
|
||||
"cooldownActive"
|
||||
],
|
||||
"title": "CooldownActiveAddCreditsNudgeEmailResultStatus",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status"
|
||||
],
|
||||
"title": "CooldownActiveAddCreditsNudgeEmailResult",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"enum": [
|
||||
"failed"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message",
|
||||
"status"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"AgentMessageDeltaNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -5667,6 +5536,13 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"AutoReviewDecisionSource": {
|
||||
"description": "[UNSTABLE] Source that produced a terminal guardian approval review decision.",
|
||||
"enum": [
|
||||
"agent"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -8142,24 +8018,8 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"isWorkspaceOwner": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"requiresOpenaiAuth": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"workspaceRole": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/WorkspaceRole"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -8720,17 +8580,28 @@
|
||||
},
|
||||
"ItemGuardianApprovalReviewCompletedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "[UNSTABLE] Temporary notification payload for guardian automatic approval review. This shape is expected to change soon.\n\nTODO(ccunningham): Attach guardian review state to the reviewed tool item's lifecycle instead of sending separate standalone review notifications so the app-server API can persist and replay review state via `thread/read`.",
|
||||
"description": "[UNSTABLE] Temporary notification payload for guardian automatic approval review. This shape is expected to change soon.",
|
||||
"properties": {
|
||||
"action": {
|
||||
"$ref": "#/definitions/v2/GuardianApprovalReviewAction"
|
||||
},
|
||||
"decisionSource": {
|
||||
"$ref": "#/definitions/v2/AutoReviewDecisionSource"
|
||||
},
|
||||
"review": {
|
||||
"$ref": "#/definitions/v2/GuardianApprovalReview"
|
||||
},
|
||||
"targetItemId": {
|
||||
"reviewId": {
|
||||
"description": "Stable identifier for this review.",
|
||||
"type": "string"
|
||||
},
|
||||
"targetItemId": {
|
||||
"description": "Identifier for the reviewed item or tool call when one exists.\n\nIn most cases, one review maps to one target item. The exceptions are - execve reviews, where a single command may contain multiple execve calls to review (only possible when using the shell_zsh_fork feature) - network policy reviews, where there is no target item\n\nA network call is triggered by a CommandExecution item, so having a target_item_id set to the CommandExecution item would be misleading because the review is about the network call, not the command execution. Therefore, target_item_id is set to None for network policy reviews.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -8740,8 +8611,9 @@
|
||||
},
|
||||
"required": [
|
||||
"action",
|
||||
"decisionSource",
|
||||
"review",
|
||||
"targetItemId",
|
||||
"reviewId",
|
||||
"threadId",
|
||||
"turnId"
|
||||
],
|
||||
@@ -8750,7 +8622,7 @@
|
||||
},
|
||||
"ItemGuardianApprovalReviewStartedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "[UNSTABLE] Temporary notification payload for guardian automatic approval review. This shape is expected to change soon.\n\nTODO(ccunningham): Attach guardian review state to the reviewed tool item's lifecycle instead of sending separate standalone review notifications so the app-server API can persist and replay review state via `thread/read`.",
|
||||
"description": "[UNSTABLE] Temporary notification payload for guardian automatic approval review. This shape is expected to change soon.",
|
||||
"properties": {
|
||||
"action": {
|
||||
"$ref": "#/definitions/v2/GuardianApprovalReviewAction"
|
||||
@@ -8758,9 +8630,17 @@
|
||||
"review": {
|
||||
"$ref": "#/definitions/v2/GuardianApprovalReview"
|
||||
},
|
||||
"targetItemId": {
|
||||
"reviewId": {
|
||||
"description": "Stable identifier for this review.",
|
||||
"type": "string"
|
||||
},
|
||||
"targetItemId": {
|
||||
"description": "Identifier for the reviewed item or tool call when one exists.\n\nIn most cases, one review maps to one target item. The exceptions are - execve reviews, where a single command may contain multiple execve calls to review (only possible when using the shell_zsh_fork feature) - network policy reviews, where there is no target item\n\nA network call is triggered by a CommandExecution item, so having a target_item_id set to the CommandExecution item would be misleading because the review is about the network call, not the command execution. Therefore, target_item_id is set to None for network policy reviews.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -8771,7 +8651,7 @@
|
||||
"required": [
|
||||
"action",
|
||||
"review",
|
||||
"targetItemId",
|
||||
"reviewId",
|
||||
"threadId",
|
||||
"turnId"
|
||||
],
|
||||
@@ -10544,16 +10424,6 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"spendControl": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/SpendControlSnapshot"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -12377,17 +12247,6 @@
|
||||
"title": "SkillsListResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"SpendControlSnapshot": {
|
||||
"properties": {
|
||||
"reached": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"reached"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SubAgentSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -12679,24 +12538,6 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadAddCreditsNudgeEmailParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadAddCreditsNudgeEmailParams",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadAddCreditsNudgeEmailResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "ThreadAddCreditsNudgeEmailResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadArchiveParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -14427,6 +14268,16 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sessionStartSource": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ThreadStartSource"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "ThreadStartParams",
|
||||
@@ -14494,6 +14345,13 @@
|
||||
"title": "ThreadStartResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadStartSource": {
|
||||
"enum": [
|
||||
"startup",
|
||||
"clear"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadStartedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -15605,14 +15463,6 @@
|
||||
"title": "WindowsWorldWritableWarningNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"WorkspaceRole": {
|
||||
"enum": [
|
||||
"account-owner",
|
||||
"account-admin",
|
||||
"standard-user"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"WriteStatus": {
|
||||
"enum": [
|
||||
"ok",
|
||||
|
||||
+50
-200
@@ -100,12 +100,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"isWorkspaceOwner": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"planType": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -115,92 +109,11 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"workspaceRole": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/WorkspaceRole"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "AccountUpdatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"AddCreditsNudgeEmailNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"result": {
|
||||
"$ref": "#/definitions/AddCreditsNudgeEmailResult"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"result",
|
||||
"threadId"
|
||||
],
|
||||
"title": "AddCreditsNudgeEmailNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"AddCreditsNudgeEmailResult": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"status": {
|
||||
"enum": [
|
||||
"sent"
|
||||
],
|
||||
"title": "SentAddCreditsNudgeEmailResultStatus",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status"
|
||||
],
|
||||
"title": "SentAddCreditsNudgeEmailResult",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"status": {
|
||||
"enum": [
|
||||
"cooldownActive"
|
||||
],
|
||||
"title": "CooldownActiveAddCreditsNudgeEmailResultStatus",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status"
|
||||
],
|
||||
"title": "CooldownActiveAddCreditsNudgeEmailResult",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"enum": [
|
||||
"failed"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message",
|
||||
"status"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"AgentMessageDeltaNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -802,6 +715,13 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"AutoReviewDecisionSource": {
|
||||
"description": "[UNSTABLE] Source that produced a terminal guardian approval review decision.",
|
||||
"enum": [
|
||||
"agent"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -1144,30 +1064,6 @@
|
||||
"title": "Thread/shellCommandRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/addCreditsNudgeEmail"
|
||||
],
|
||||
"title": "Thread/addCreditsNudgeEmailRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadAddCreditsNudgeEmailParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/addCreditsNudgeEmailRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -4881,24 +4777,8 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"isWorkspaceOwner": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"requiresOpenaiAuth": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"workspaceRole": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/WorkspaceRole"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -5503,17 +5383,28 @@
|
||||
},
|
||||
"ItemGuardianApprovalReviewCompletedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "[UNSTABLE] Temporary notification payload for guardian automatic approval review. This shape is expected to change soon.\n\nTODO(ccunningham): Attach guardian review state to the reviewed tool item's lifecycle instead of sending separate standalone review notifications so the app-server API can persist and replay review state via `thread/read`.",
|
||||
"description": "[UNSTABLE] Temporary notification payload for guardian automatic approval review. This shape is expected to change soon.",
|
||||
"properties": {
|
||||
"action": {
|
||||
"$ref": "#/definitions/GuardianApprovalReviewAction"
|
||||
},
|
||||
"decisionSource": {
|
||||
"$ref": "#/definitions/AutoReviewDecisionSource"
|
||||
},
|
||||
"review": {
|
||||
"$ref": "#/definitions/GuardianApprovalReview"
|
||||
},
|
||||
"targetItemId": {
|
||||
"reviewId": {
|
||||
"description": "Stable identifier for this review.",
|
||||
"type": "string"
|
||||
},
|
||||
"targetItemId": {
|
||||
"description": "Identifier for the reviewed item or tool call when one exists.\n\nIn most cases, one review maps to one target item. The exceptions are - execve reviews, where a single command may contain multiple execve calls to review (only possible when using the shell_zsh_fork feature) - network policy reviews, where there is no target item\n\nA network call is triggered by a CommandExecution item, so having a target_item_id set to the CommandExecution item would be misleading because the review is about the network call, not the command execution. Therefore, target_item_id is set to None for network policy reviews.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -5523,8 +5414,9 @@
|
||||
},
|
||||
"required": [
|
||||
"action",
|
||||
"decisionSource",
|
||||
"review",
|
||||
"targetItemId",
|
||||
"reviewId",
|
||||
"threadId",
|
||||
"turnId"
|
||||
],
|
||||
@@ -5533,7 +5425,7 @@
|
||||
},
|
||||
"ItemGuardianApprovalReviewStartedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "[UNSTABLE] Temporary notification payload for guardian automatic approval review. This shape is expected to change soon.\n\nTODO(ccunningham): Attach guardian review state to the reviewed tool item's lifecycle instead of sending separate standalone review notifications so the app-server API can persist and replay review state via `thread/read`.",
|
||||
"description": "[UNSTABLE] Temporary notification payload for guardian automatic approval review. This shape is expected to change soon.",
|
||||
"properties": {
|
||||
"action": {
|
||||
"$ref": "#/definitions/GuardianApprovalReviewAction"
|
||||
@@ -5541,9 +5433,17 @@
|
||||
"review": {
|
||||
"$ref": "#/definitions/GuardianApprovalReview"
|
||||
},
|
||||
"targetItemId": {
|
||||
"reviewId": {
|
||||
"description": "Stable identifier for this review.",
|
||||
"type": "string"
|
||||
},
|
||||
"targetItemId": {
|
||||
"description": "Identifier for the reviewed item or tool call when one exists.\n\nIn most cases, one review maps to one target item. The exceptions are - execve reviews, where a single command may contain multiple execve calls to review (only possible when using the shell_zsh_fork feature) - network policy reviews, where there is no target item\n\nA network call is triggered by a CommandExecution item, so having a target_item_id set to the CommandExecution item would be misleading because the review is about the network call, not the command execution. Therefore, target_item_id is set to None for network policy reviews.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -5554,7 +5454,7 @@
|
||||
"required": [
|
||||
"action",
|
||||
"review",
|
||||
"targetItemId",
|
||||
"reviewId",
|
||||
"threadId",
|
||||
"turnId"
|
||||
],
|
||||
@@ -7327,16 +7227,6 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"spendControl": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SpendControlSnapshot"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -9346,26 +9236,6 @@
|
||||
"title": "Account/rateLimits/updatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"account/addCreditsNudgeEmail/completed"
|
||||
],
|
||||
"title": "Account/addCreditsNudgeEmail/completedNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/AddCreditsNudgeEmailNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Account/addCreditsNudgeEmail/completedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
@@ -10232,17 +10102,6 @@
|
||||
"title": "SkillsListResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"SpendControlSnapshot": {
|
||||
"properties": {
|
||||
"reached": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"reached"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SubAgentSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -10534,24 +10393,6 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadAddCreditsNudgeEmailParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadAddCreditsNudgeEmailParams",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadAddCreditsNudgeEmailResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "ThreadAddCreditsNudgeEmailResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadArchiveParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -12282,6 +12123,16 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sessionStartSource": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadStartSource"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "ThreadStartParams",
|
||||
@@ -12349,6 +12200,13 @@
|
||||
"title": "ThreadStartResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadStartSource": {
|
||||
"enum": [
|
||||
"startup",
|
||||
"clear"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadStartedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -13460,14 +13318,6 @@
|
||||
"title": "WindowsWorldWritableWarningNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"WorkspaceRole": {
|
||||
"enum": [
|
||||
"account-owner",
|
||||
"account-admin",
|
||||
"standard-user"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"WriteStatus": {
|
||||
"enum": [
|
||||
"ok",
|
||||
|
||||
@@ -91,16 +91,6 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"spendControl": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SpendControlSnapshot"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -130,17 +120,6 @@
|
||||
"usedPercent"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SpendControlSnapshot": {
|
||||
"properties": {
|
||||
"reached": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"reached"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
|
||||
@@ -42,14 +42,6 @@
|
||||
"unknown"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"WorkspaceRole": {
|
||||
"enum": [
|
||||
"account-owner",
|
||||
"account-admin",
|
||||
"standard-user"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
@@ -63,12 +55,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"isWorkspaceOwner": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"planType": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -78,16 +64,6 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"workspaceRole": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/WorkspaceRole"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "AccountUpdatedNotification",
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"AddCreditsNudgeEmailResult": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"status": {
|
||||
"enum": [
|
||||
"sent"
|
||||
],
|
||||
"title": "SentAddCreditsNudgeEmailResultStatus",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status"
|
||||
],
|
||||
"title": "SentAddCreditsNudgeEmailResult",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"status": {
|
||||
"enum": [
|
||||
"cooldownActive"
|
||||
],
|
||||
"title": "CooldownActiveAddCreditsNudgeEmailResultStatus",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status"
|
||||
],
|
||||
"title": "CooldownActiveAddCreditsNudgeEmailResult",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"enum": [
|
||||
"failed"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message",
|
||||
"status"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"result": {
|
||||
"$ref": "#/definitions/AddCreditsNudgeEmailResult"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"result",
|
||||
"threadId"
|
||||
],
|
||||
"title": "AddCreditsNudgeEmailNotification",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -91,16 +91,6 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"spendControl": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SpendControlSnapshot"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -130,17 +120,6 @@
|
||||
"usedPercent"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SpendControlSnapshot": {
|
||||
"properties": {
|
||||
"reached": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"reached"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
|
||||
@@ -60,14 +60,6 @@
|
||||
"unknown"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"WorkspaceRole": {
|
||||
"enum": [
|
||||
"account-owner",
|
||||
"account-admin",
|
||||
"standard-user"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
@@ -81,24 +73,8 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"isWorkspaceOwner": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"requiresOpenaiAuth": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"workspaceRole": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/WorkspaceRole"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
+22
-3
@@ -1,6 +1,13 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"AutoReviewDecisionSource": {
|
||||
"description": "[UNSTABLE] Source that produced a terminal guardian approval review decision.",
|
||||
"enum": [
|
||||
"agent"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"GuardianApprovalReview": {
|
||||
"description": "[UNSTABLE] Temporary guardian approval review payload used by `item/autoApprovalReview/*` notifications. This shape is expected to change soon.",
|
||||
"properties": {
|
||||
@@ -256,17 +263,28 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "[UNSTABLE] Temporary notification payload for guardian automatic approval review. This shape is expected to change soon.\n\nTODO(ccunningham): Attach guardian review state to the reviewed tool item's lifecycle instead of sending separate standalone review notifications so the app-server API can persist and replay review state via `thread/read`.",
|
||||
"description": "[UNSTABLE] Temporary notification payload for guardian automatic approval review. This shape is expected to change soon.",
|
||||
"properties": {
|
||||
"action": {
|
||||
"$ref": "#/definitions/GuardianApprovalReviewAction"
|
||||
},
|
||||
"decisionSource": {
|
||||
"$ref": "#/definitions/AutoReviewDecisionSource"
|
||||
},
|
||||
"review": {
|
||||
"$ref": "#/definitions/GuardianApprovalReview"
|
||||
},
|
||||
"targetItemId": {
|
||||
"reviewId": {
|
||||
"description": "Stable identifier for this review.",
|
||||
"type": "string"
|
||||
},
|
||||
"targetItemId": {
|
||||
"description": "Identifier for the reviewed item or tool call when one exists.\n\nIn most cases, one review maps to one target item. The exceptions are - execve reviews, where a single command may contain multiple execve calls to review (only possible when using the shell_zsh_fork feature) - network policy reviews, where there is no target item\n\nA network call is triggered by a CommandExecution item, so having a target_item_id set to the CommandExecution item would be misleading because the review is about the network call, not the command execution. Therefore, target_item_id is set to None for network policy reviews.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -276,8 +294,9 @@
|
||||
},
|
||||
"required": [
|
||||
"action",
|
||||
"decisionSource",
|
||||
"review",
|
||||
"targetItemId",
|
||||
"reviewId",
|
||||
"threadId",
|
||||
"turnId"
|
||||
],
|
||||
|
||||
+11
-3
@@ -256,7 +256,7 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "[UNSTABLE] Temporary notification payload for guardian automatic approval review. This shape is expected to change soon.\n\nTODO(ccunningham): Attach guardian review state to the reviewed tool item's lifecycle instead of sending separate standalone review notifications so the app-server API can persist and replay review state via `thread/read`.",
|
||||
"description": "[UNSTABLE] Temporary notification payload for guardian automatic approval review. This shape is expected to change soon.",
|
||||
"properties": {
|
||||
"action": {
|
||||
"$ref": "#/definitions/GuardianApprovalReviewAction"
|
||||
@@ -264,9 +264,17 @@
|
||||
"review": {
|
||||
"$ref": "#/definitions/GuardianApprovalReview"
|
||||
},
|
||||
"targetItemId": {
|
||||
"reviewId": {
|
||||
"description": "Stable identifier for this review.",
|
||||
"type": "string"
|
||||
},
|
||||
"targetItemId": {
|
||||
"description": "Identifier for the reviewed item or tool call when one exists.\n\nIn most cases, one review maps to one target item. The exceptions are - execve reviews, where a single command may contain multiple execve calls to review (only possible when using the shell_zsh_fork feature) - network policy reviews, where there is no target item\n\nA network call is triggered by a CommandExecution item, so having a target_item_id set to the CommandExecution item would be misleading because the review is about the network call, not the command execution. Therefore, target_item_id is set to None for network policy reviews.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -277,7 +285,7 @@
|
||||
"required": [
|
||||
"action",
|
||||
"review",
|
||||
"targetItemId",
|
||||
"reviewId",
|
||||
"threadId",
|
||||
"turnId"
|
||||
],
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadAddCreditsNudgeEmailParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "ThreadAddCreditsNudgeEmailResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -101,6 +101,13 @@
|
||||
"flex"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadStartSource": {
|
||||
"enum": [
|
||||
"startup",
|
||||
"clear"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
@@ -210,6 +217,16 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sessionStartSource": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadStartSource"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "ThreadStartParams",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -6,7 +6,6 @@ import type { FuzzyFileSearchSessionUpdatedNotification } from "./FuzzyFileSearc
|
||||
import type { AccountLoginCompletedNotification } from "./v2/AccountLoginCompletedNotification";
|
||||
import type { AccountRateLimitsUpdatedNotification } from "./v2/AccountRateLimitsUpdatedNotification";
|
||||
import type { AccountUpdatedNotification } from "./v2/AccountUpdatedNotification";
|
||||
import type { AddCreditsNudgeEmailNotification } from "./v2/AddCreditsNudgeEmailNotification";
|
||||
import type { AgentMessageDeltaNotification } from "./v2/AgentMessageDeltaNotification";
|
||||
import type { AppListUpdatedNotification } from "./v2/AppListUpdatedNotification";
|
||||
import type { CommandExecOutputDeltaNotification } from "./v2/CommandExecOutputDeltaNotification";
|
||||
@@ -59,4 +58,4 @@ import type { WindowsWorldWritableWarningNotification } from "./v2/WindowsWorldW
|
||||
/**
|
||||
* Notification sent from the server to the client.
|
||||
*/
|
||||
export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "account/addCreditsNudgeEmail/completed", "params": AddCreditsNudgeEmailNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/transcriptUpdated", "params": ThreadRealtimeTranscriptUpdatedNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/sdp", "params": ThreadRealtimeSdpNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification };
|
||||
export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/transcriptUpdated", "params": ThreadRealtimeTranscriptUpdatedNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/sdp", "params": ThreadRealtimeSdpNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification };
|
||||
|
||||
@@ -3,6 +3,5 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AuthMode } from "../AuthMode";
|
||||
import type { PlanType } from "../PlanType";
|
||||
import type { WorkspaceRole } from "./WorkspaceRole";
|
||||
|
||||
export type AccountUpdatedNotification = { authMode: AuthMode | null, planType: PlanType | null, workspaceRole: WorkspaceRole | null, isWorkspaceOwner: boolean | null, };
|
||||
export type AccountUpdatedNotification = { authMode: AuthMode | null, planType: PlanType | null, };
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AddCreditsNudgeEmailResult } from "./AddCreditsNudgeEmailResult";
|
||||
|
||||
export type AddCreditsNudgeEmailNotification = { threadId: string, result: AddCreditsNudgeEmailResult, };
|
||||
@@ -1,5 +0,0 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type AddCreditsNudgeEmailResult = { "status": "sent" } | { "status": "cooldownActive" } | { "status": "failed", message: string, };
|
||||
+4
-1
@@ -2,4 +2,7 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ThreadAddCreditsNudgeEmailResponse = Record<string, never>;
|
||||
/**
|
||||
* [UNSTABLE] Source that produced a terminal guardian approval review decision.
|
||||
*/
|
||||
export type AutoReviewDecisionSource = "agent";
|
||||
@@ -2,6 +2,5 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Account } from "./Account";
|
||||
import type { WorkspaceRole } from "./WorkspaceRole";
|
||||
|
||||
export type GetAccountResponse = { account: Account | null, workspaceRole: WorkspaceRole | null, isWorkspaceOwner: boolean | null, requiresOpenaiAuth: boolean, };
|
||||
export type GetAccountResponse = { account: Account | null, requiresOpenaiAuth: boolean, };
|
||||
|
||||
+20
-5
@@ -1,15 +1,30 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AutoReviewDecisionSource } from "./AutoReviewDecisionSource";
|
||||
import type { GuardianApprovalReview } from "./GuardianApprovalReview";
|
||||
import type { GuardianApprovalReviewAction } from "./GuardianApprovalReviewAction";
|
||||
|
||||
/**
|
||||
* [UNSTABLE] Temporary notification payload for guardian automatic approval
|
||||
* review. This shape is expected to change soon.
|
||||
*
|
||||
* TODO(ccunningham): Attach guardian review state to the reviewed tool item's
|
||||
* lifecycle instead of sending separate standalone review notifications so the
|
||||
* app-server API can persist and replay review state via `thread/read`.
|
||||
*/
|
||||
export type ItemGuardianApprovalReviewCompletedNotification = { threadId: string, turnId: string, targetItemId: string, review: GuardianApprovalReview, action: GuardianApprovalReviewAction, };
|
||||
export type ItemGuardianApprovalReviewCompletedNotification = { threadId: string, turnId: string,
|
||||
/**
|
||||
* Stable identifier for this review.
|
||||
*/
|
||||
reviewId: string,
|
||||
/**
|
||||
* Identifier for the reviewed item or tool call when one exists.
|
||||
*
|
||||
* In most cases, one review maps to one target item. The exceptions are
|
||||
* - execve reviews, where a single command may contain multiple execve
|
||||
* calls to review (only possible when using the shell_zsh_fork feature)
|
||||
* - network policy reviews, where there is no target item
|
||||
*
|
||||
* A network call is triggered by a CommandExecution item, so having a
|
||||
* target_item_id set to the CommandExecution item would be misleading
|
||||
* because the review is about the network call, not the command execution.
|
||||
* Therefore, target_item_id is set to None for network policy reviews.
|
||||
*/
|
||||
targetItemId: string | null, decisionSource: AutoReviewDecisionSource, review: GuardianApprovalReview, action: GuardianApprovalReviewAction, };
|
||||
|
||||
+19
-5
@@ -7,9 +7,23 @@ import type { GuardianApprovalReviewAction } from "./GuardianApprovalReviewActio
|
||||
/**
|
||||
* [UNSTABLE] Temporary notification payload for guardian automatic approval
|
||||
* review. This shape is expected to change soon.
|
||||
*
|
||||
* TODO(ccunningham): Attach guardian review state to the reviewed tool item's
|
||||
* lifecycle instead of sending separate standalone review notifications so the
|
||||
* app-server API can persist and replay review state via `thread/read`.
|
||||
*/
|
||||
export type ItemGuardianApprovalReviewStartedNotification = { threadId: string, turnId: string, targetItemId: string, review: GuardianApprovalReview, action: GuardianApprovalReviewAction, };
|
||||
export type ItemGuardianApprovalReviewStartedNotification = { threadId: string, turnId: string,
|
||||
/**
|
||||
* Stable identifier for this review.
|
||||
*/
|
||||
reviewId: string,
|
||||
/**
|
||||
* Identifier for the reviewed item or tool call when one exists.
|
||||
*
|
||||
* In most cases, one review maps to one target item. The exceptions are
|
||||
* - execve reviews, where a single command may contain multiple execve
|
||||
* calls to review (only possible when using the shell_zsh_fork feature)
|
||||
* - network policy reviews, where there is no target item
|
||||
*
|
||||
* A network call is triggered by a CommandExecution item, so having a
|
||||
* target_item_id set to the CommandExecution item would be misleading
|
||||
* because the review is about the network call, not the command execution.
|
||||
* Therefore, target_item_id is set to None for network policy reviews.
|
||||
*/
|
||||
targetItemId: string | null, review: GuardianApprovalReview, action: GuardianApprovalReviewAction, };
|
||||
|
||||
@@ -4,6 +4,5 @@
|
||||
import type { PlanType } from "../PlanType";
|
||||
import type { CreditsSnapshot } from "./CreditsSnapshot";
|
||||
import type { RateLimitWindow } from "./RateLimitWindow";
|
||||
import type { SpendControlSnapshot } from "./SpendControlSnapshot";
|
||||
|
||||
export type RateLimitSnapshot = { limitId: string | null, limitName: string | null, primary: RateLimitWindow | null, secondary: RateLimitWindow | null, credits: CreditsSnapshot | null, spendControl: SpendControlSnapshot | null, planType: PlanType | null, };
|
||||
export type RateLimitSnapshot = { limitId: string | null, limitName: string | null, primary: RateLimitWindow | null, secondary: RateLimitWindow | null, credits: CreditsSnapshot | null, planType: PlanType | null, };
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ThreadAddCreditsNudgeEmailParams = { threadId: string, };
|
||||
@@ -7,12 +7,13 @@ import type { JsonValue } from "../serde_json/JsonValue";
|
||||
import type { ApprovalsReviewer } from "./ApprovalsReviewer";
|
||||
import type { AskForApproval } from "./AskForApproval";
|
||||
import type { SandboxMode } from "./SandboxMode";
|
||||
import type { ThreadStartSource } from "./ThreadStartSource";
|
||||
|
||||
export type ThreadStartParams = {model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier | null | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, /**
|
||||
* Override where approval requests are routed for review on this thread
|
||||
* and subsequent turns.
|
||||
*/
|
||||
approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, ephemeral?: boolean | null, /**
|
||||
approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, ephemeral?: boolean | null, sessionStartSource?: ThreadStartSource | null, /**
|
||||
* If true, opt into emitting raw Responses API items on the event stream.
|
||||
* This is for internal use only (e.g. Codex Cloud).
|
||||
*/
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type SpendControlSnapshot = { reached: boolean, };
|
||||
export type ThreadStartSource = "startup" | "clear";
|
||||
@@ -1,5 +0,0 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type WorkspaceRole = "account-owner" | "account-admin" | "standard-user";
|
||||
@@ -4,8 +4,6 @@ export type { Account } from "./Account";
|
||||
export type { AccountLoginCompletedNotification } from "./AccountLoginCompletedNotification";
|
||||
export type { AccountRateLimitsUpdatedNotification } from "./AccountRateLimitsUpdatedNotification";
|
||||
export type { AccountUpdatedNotification } from "./AccountUpdatedNotification";
|
||||
export type { AddCreditsNudgeEmailNotification } from "./AddCreditsNudgeEmailNotification";
|
||||
export type { AddCreditsNudgeEmailResult } from "./AddCreditsNudgeEmailResult";
|
||||
export type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions";
|
||||
export type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions";
|
||||
export type { AdditionalPermissionProfile } from "./AdditionalPermissionProfile";
|
||||
@@ -26,6 +24,7 @@ export type { AppsDefaultConfig } from "./AppsDefaultConfig";
|
||||
export type { AppsListParams } from "./AppsListParams";
|
||||
export type { AppsListResponse } from "./AppsListResponse";
|
||||
export type { AskForApproval } from "./AskForApproval";
|
||||
export type { AutoReviewDecisionSource } from "./AutoReviewDecisionSource";
|
||||
export type { ByteRange } from "./ByteRange";
|
||||
export type { CancelLoginAccountParams } from "./CancelLoginAccountParams";
|
||||
export type { CancelLoginAccountResponse } from "./CancelLoginAccountResponse";
|
||||
@@ -267,15 +266,12 @@ export type { SkillsListEntry } from "./SkillsListEntry";
|
||||
export type { SkillsListExtraRootsForCwd } from "./SkillsListExtraRootsForCwd";
|
||||
export type { SkillsListParams } from "./SkillsListParams";
|
||||
export type { SkillsListResponse } from "./SkillsListResponse";
|
||||
export type { SpendControlSnapshot } from "./SpendControlSnapshot";
|
||||
export type { TerminalInteractionNotification } from "./TerminalInteractionNotification";
|
||||
export type { TextElement } from "./TextElement";
|
||||
export type { TextPosition } from "./TextPosition";
|
||||
export type { TextRange } from "./TextRange";
|
||||
export type { Thread } from "./Thread";
|
||||
export type { ThreadActiveFlag } from "./ThreadActiveFlag";
|
||||
export type { ThreadAddCreditsNudgeEmailParams } from "./ThreadAddCreditsNudgeEmailParams";
|
||||
export type { ThreadAddCreditsNudgeEmailResponse } from "./ThreadAddCreditsNudgeEmailResponse";
|
||||
export type { ThreadArchiveParams } from "./ThreadArchiveParams";
|
||||
export type { ThreadArchiveResponse } from "./ThreadArchiveResponse";
|
||||
export type { ThreadArchivedNotification } from "./ThreadArchivedNotification";
|
||||
@@ -316,6 +312,7 @@ export type { ThreadSortKey } from "./ThreadSortKey";
|
||||
export type { ThreadSourceKind } from "./ThreadSourceKind";
|
||||
export type { ThreadStartParams } from "./ThreadStartParams";
|
||||
export type { ThreadStartResponse } from "./ThreadStartResponse";
|
||||
export type { ThreadStartSource } from "./ThreadStartSource";
|
||||
export type { ThreadStartedNotification } from "./ThreadStartedNotification";
|
||||
export type { ThreadStatus } from "./ThreadStatus";
|
||||
export type { ThreadStatusChangedNotification } from "./ThreadStatusChangedNotification";
|
||||
@@ -356,5 +353,4 @@ export type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode";
|
||||
export type { WindowsSandboxSetupStartParams } from "./WindowsSandboxSetupStartParams";
|
||||
export type { WindowsSandboxSetupStartResponse } from "./WindowsSandboxSetupStartResponse";
|
||||
export type { WindowsWorldWritableWarningNotification } from "./WindowsWorldWritableWarningNotification";
|
||||
export type { WorkspaceRole } from "./WorkspaceRole";
|
||||
export type { WriteStatus } from "./WriteStatus";
|
||||
|
||||
@@ -296,10 +296,6 @@ client_request_definitions! {
|
||||
params: v2::ThreadShellCommandParams,
|
||||
response: v2::ThreadShellCommandResponse,
|
||||
},
|
||||
ThreadAddCreditsNudgeEmail => "thread/addCreditsNudgeEmail" {
|
||||
params: v2::ThreadAddCreditsNudgeEmailParams,
|
||||
response: v2::ThreadAddCreditsNudgeEmailResponse,
|
||||
},
|
||||
#[experimental("thread/backgroundTerminals/clean")]
|
||||
ThreadBackgroundTerminalsClean => "thread/backgroundTerminals/clean" {
|
||||
params: v2::ThreadBackgroundTerminalsCleanParams,
|
||||
@@ -995,7 +991,6 @@ server_notification_definitions! {
|
||||
McpServerStatusUpdated => "mcpServer/startupStatus/updated" (v2::McpServerStatusUpdatedNotification),
|
||||
AccountUpdated => "account/updated" (v2::AccountUpdatedNotification),
|
||||
AccountRateLimitsUpdated => "account/rateLimits/updated" (v2::AccountRateLimitsUpdatedNotification),
|
||||
AddCreditsNudgeEmailCompleted => "account/addCreditsNudgeEmail/completed" (v2::AddCreditsNudgeEmailNotification),
|
||||
AppListUpdated => "app/list/updated" (v2::AppListUpdatedNotification),
|
||||
FsChanged => "fs/changed" (v2::FsChangedNotification),
|
||||
ReasoningSummaryTextDelta => "item/reasoning/summaryTextDelta" (v2::ReasoningSummaryTextDeltaNotification),
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
//! - The projection is presentation-specific. Core protocol events stay generic, while the
|
||||
//! app-server protocol decides how to surface those events as `ThreadItem`s for clients.
|
||||
use crate::protocol::common::ServerNotification;
|
||||
use crate::protocol::v2::AutoReviewDecisionSource;
|
||||
use crate::protocol::v2::CommandAction;
|
||||
use crate::protocol::v2::CommandExecutionSource;
|
||||
use crate::protocol::v2::CommandExecutionStatus;
|
||||
@@ -142,12 +143,13 @@ pub fn build_item_from_guardian_event(
|
||||
) -> Option<ThreadItem> {
|
||||
match &assessment.action {
|
||||
GuardianAssessmentAction::Command { command, cwd, .. } => {
|
||||
let id = assessment.target_item_id.as_ref()?;
|
||||
let command = command.clone();
|
||||
let command_actions = vec![CommandAction::Unknown {
|
||||
command: command.clone(),
|
||||
}];
|
||||
Some(ThreadItem::CommandExecution {
|
||||
id: assessment.id.clone(),
|
||||
id: id.clone(),
|
||||
command,
|
||||
cwd: cwd.clone(),
|
||||
process_id: None,
|
||||
@@ -162,6 +164,7 @@ pub fn build_item_from_guardian_event(
|
||||
GuardianAssessmentAction::Execve {
|
||||
program, argv, cwd, ..
|
||||
} => {
|
||||
let id = assessment.target_item_id.as_ref()?;
|
||||
let argv = if argv.is_empty() {
|
||||
vec![program.clone()]
|
||||
} else {
|
||||
@@ -179,7 +182,7 @@ pub fn build_item_from_guardian_event(
|
||||
parsed_cmd.into_iter().map(CommandAction::from).collect()
|
||||
};
|
||||
Some(ThreadItem::CommandExecution {
|
||||
id: assessment.id.clone(),
|
||||
id: id.clone(),
|
||||
command,
|
||||
cwd: cwd.clone(),
|
||||
process_id: None,
|
||||
@@ -202,9 +205,6 @@ pub fn guardian_auto_approval_review_notification(
|
||||
event_turn_id: &str,
|
||||
assessment: &GuardianAssessmentEvent,
|
||||
) -> ServerNotification {
|
||||
// TODO(ccunningham): Attach guardian review state to the reviewed tool
|
||||
// item's lifecycle instead of sending standalone review notifications so
|
||||
// the app-server API can persist and replay review state via `thread/read`.
|
||||
let turn_id = if assessment.turn_id.is_empty() {
|
||||
event_turn_id.to_string()
|
||||
} else {
|
||||
@@ -236,7 +236,8 @@ pub fn guardian_auto_approval_review_notification(
|
||||
ItemGuardianApprovalReviewStartedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id,
|
||||
target_item_id: assessment.id.clone(),
|
||||
review_id: assessment.id.clone(),
|
||||
target_item_id: assessment.target_item_id.clone(),
|
||||
review,
|
||||
action,
|
||||
},
|
||||
@@ -249,7 +250,12 @@ pub fn guardian_auto_approval_review_notification(
|
||||
ItemGuardianApprovalReviewCompletedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id,
|
||||
target_item_id: assessment.id.clone(),
|
||||
review_id: assessment.id.clone(),
|
||||
target_item_id: assessment.target_item_id.clone(),
|
||||
decision_source: assessment
|
||||
.decision_source
|
||||
.map(AutoReviewDecisionSource::from)
|
||||
.unwrap_or(AutoReviewDecisionSource::Agent),
|
||||
review,
|
||||
action,
|
||||
},
|
||||
|
||||
@@ -2088,12 +2088,14 @@ mod tests {
|
||||
local_images: Vec::new(),
|
||||
}),
|
||||
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||
id: "guardian-exec".into(),
|
||||
id: "review-guardian-exec".into(),
|
||||
target_item_id: Some("guardian-exec".into()),
|
||||
turn_id: "turn-1".into(),
|
||||
status: GuardianAssessmentStatus::InProgress,
|
||||
risk_level: None,
|
||||
user_authorization: None,
|
||||
rationale: None,
|
||||
decision_source: None,
|
||||
action: serde_json::from_value(serde_json::json!({
|
||||
"type": "command",
|
||||
"source": "shell",
|
||||
@@ -2103,12 +2105,16 @@ mod tests {
|
||||
.expect("guardian action"),
|
||||
}),
|
||||
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||
id: "guardian-exec".into(),
|
||||
id: "review-guardian-exec".into(),
|
||||
target_item_id: Some("guardian-exec".into()),
|
||||
turn_id: "turn-1".into(),
|
||||
status: GuardianAssessmentStatus::Denied,
|
||||
risk_level: Some(codex_protocol::protocol::GuardianRiskLevel::High),
|
||||
user_authorization: Some(codex_protocol::protocol::GuardianUserAuthorization::Low),
|
||||
rationale: Some("Would delete user data.".into()),
|
||||
decision_source: Some(
|
||||
codex_protocol::protocol::GuardianAssessmentDecisionSource::Agent,
|
||||
),
|
||||
action: serde_json::from_value(serde_json::json!({
|
||||
"type": "command",
|
||||
"source": "shell",
|
||||
@@ -2161,12 +2167,14 @@ mod tests {
|
||||
local_images: Vec::new(),
|
||||
}),
|
||||
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||
id: "guardian-execve".into(),
|
||||
id: "review-guardian-execve".into(),
|
||||
target_item_id: Some("guardian-execve".into()),
|
||||
turn_id: "turn-1".into(),
|
||||
status: GuardianAssessmentStatus::InProgress,
|
||||
risk_level: None,
|
||||
user_authorization: None,
|
||||
rationale: None,
|
||||
decision_source: None,
|
||||
action: serde_json::from_value(serde_json::json!({
|
||||
"type": "execve",
|
||||
"source": "shell",
|
||||
|
||||
@@ -9,6 +9,7 @@ use codex_protocol::account::PlanType;
|
||||
use codex_protocol::approvals::ElicitationRequest as CoreElicitationRequest;
|
||||
use codex_protocol::approvals::ExecPolicyAmendment as CoreExecPolicyAmendment;
|
||||
use codex_protocol::approvals::GuardianAssessmentAction as CoreGuardianAssessmentAction;
|
||||
use codex_protocol::approvals::GuardianAssessmentDecisionSource as CoreGuardianAssessmentDecisionSource;
|
||||
use codex_protocol::approvals::GuardianCommandSource as CoreGuardianCommandSource;
|
||||
use codex_protocol::approvals::NetworkApprovalContext as CoreNetworkApprovalContext;
|
||||
use codex_protocol::approvals::NetworkApprovalProtocol as CoreNetworkApprovalProtocol;
|
||||
@@ -82,7 +83,6 @@ use codex_protocol::protocol::SkillInterface as CoreSkillInterface;
|
||||
use codex_protocol::protocol::SkillMetadata as CoreSkillMetadata;
|
||||
use codex_protocol::protocol::SkillScope as CoreSkillScope;
|
||||
use codex_protocol::protocol::SkillToolDependency as CoreSkillToolDependency;
|
||||
use codex_protocol::protocol::SpendControlSnapshot as CoreSpendControlSnapshot;
|
||||
use codex_protocol::protocol::SubAgentSource as CoreSubAgentSource;
|
||||
use codex_protocol::protocol::TokenUsage as CoreTokenUsage;
|
||||
use codex_protocol::protocol::TokenUsageInfo as CoreTokenUsageInfo;
|
||||
@@ -413,6 +413,14 @@ v2_enum_from_core!(
|
||||
}
|
||||
);
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase", export_to = "v2/")]
|
||||
pub enum ThreadStartSource {
|
||||
Startup,
|
||||
Clear,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -1739,27 +1747,11 @@ pub struct GetAccountParams {
|
||||
pub refresh_token: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum WorkspaceRole {
|
||||
#[serde(rename = "account-owner")]
|
||||
#[ts(rename = "account-owner")]
|
||||
AccountOwner,
|
||||
#[serde(rename = "account-admin")]
|
||||
#[ts(rename = "account-admin")]
|
||||
AccountAdmin,
|
||||
#[serde(rename = "standard-user")]
|
||||
#[ts(rename = "standard-user")]
|
||||
StandardUser,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct GetAccountResponse {
|
||||
pub account: Option<Account>,
|
||||
pub workspace_role: Option<WorkspaceRole>,
|
||||
pub is_workspace_owner: Option<bool>,
|
||||
pub requires_openai_auth: bool,
|
||||
}
|
||||
|
||||
@@ -2633,6 +2625,8 @@ pub struct ThreadStartParams {
|
||||
pub personality: Option<Personality>,
|
||||
#[ts(optional = nullable)]
|
||||
pub ephemeral: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub session_start_source: Option<ThreadStartSource>,
|
||||
#[experimental("thread/start.dynamicTools")]
|
||||
#[ts(optional = nullable)]
|
||||
pub dynamic_tools: Option<Vec<DynamicToolSpec>>,
|
||||
@@ -3036,18 +3030,6 @@ pub struct ThreadShellCommandParams {
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadShellCommandResponse {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadAddCreditsNudgeEmailParams {
|
||||
pub thread_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadAddCreditsNudgeEmailResponse {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -3685,8 +3667,6 @@ pub struct Thread {
|
||||
pub struct AccountUpdatedNotification {
|
||||
pub auth_mode: Option<AuthMode>,
|
||||
pub plan_type: Option<PlanType>,
|
||||
pub workspace_role: Option<WorkspaceRole>,
|
||||
pub is_workspace_owner: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
@@ -4555,6 +4535,22 @@ pub enum GuardianApprovalReviewStatus {
|
||||
Aborted,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
/// [UNSTABLE] Source that produced a terminal guardian approval review decision.
|
||||
pub enum AutoReviewDecisionSource {
|
||||
Agent,
|
||||
}
|
||||
|
||||
impl From<CoreGuardianAssessmentDecisionSource> for AutoReviewDecisionSource {
|
||||
fn from(value: CoreGuardianAssessmentDecisionSource) -> Self {
|
||||
match value {
|
||||
CoreGuardianAssessmentDecisionSource::Agent => Self::Agent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -5311,14 +5307,23 @@ pub struct ItemStartedNotification {
|
||||
#[ts(export_to = "v2/")]
|
||||
/// [UNSTABLE] Temporary notification payload for guardian automatic approval
|
||||
/// review. This shape is expected to change soon.
|
||||
///
|
||||
/// TODO(ccunningham): Attach guardian review state to the reviewed tool item's
|
||||
/// lifecycle instead of sending separate standalone review notifications so the
|
||||
/// app-server API can persist and replay review state via `thread/read`.
|
||||
pub struct ItemGuardianApprovalReviewStartedNotification {
|
||||
pub thread_id: String,
|
||||
pub turn_id: String,
|
||||
pub target_item_id: String,
|
||||
/// Stable identifier for this review.
|
||||
pub review_id: String,
|
||||
/// Identifier for the reviewed item or tool call when one exists.
|
||||
///
|
||||
/// In most cases, one review maps to one target item. The exceptions are
|
||||
/// - execve reviews, where a single command may contain multiple execve
|
||||
/// calls to review (only possible when using the shell_zsh_fork feature)
|
||||
/// - network policy reviews, where there is no target item
|
||||
///
|
||||
/// A network call is triggered by a CommandExecution item, so having a
|
||||
/// target_item_id set to the CommandExecution item would be misleading
|
||||
/// because the review is about the network call, not the command execution.
|
||||
/// Therefore, target_item_id is set to None for network policy reviews.
|
||||
pub target_item_id: Option<String>,
|
||||
pub review: GuardianApprovalReview,
|
||||
pub action: GuardianApprovalReviewAction,
|
||||
}
|
||||
@@ -5328,14 +5333,24 @@ pub struct ItemGuardianApprovalReviewStartedNotification {
|
||||
#[ts(export_to = "v2/")]
|
||||
/// [UNSTABLE] Temporary notification payload for guardian automatic approval
|
||||
/// review. This shape is expected to change soon.
|
||||
///
|
||||
/// TODO(ccunningham): Attach guardian review state to the reviewed tool item's
|
||||
/// lifecycle instead of sending separate standalone review notifications so the
|
||||
/// app-server API can persist and replay review state via `thread/read`.
|
||||
pub struct ItemGuardianApprovalReviewCompletedNotification {
|
||||
pub thread_id: String,
|
||||
pub turn_id: String,
|
||||
pub target_item_id: String,
|
||||
/// Stable identifier for this review.
|
||||
pub review_id: String,
|
||||
/// Identifier for the reviewed item or tool call when one exists.
|
||||
///
|
||||
/// In most cases, one review maps to one target item. The exceptions are
|
||||
/// - execve reviews, where a single command may contain multiple execve
|
||||
/// calls to review (only possible when using the shell_zsh_fork feature)
|
||||
/// - network policy reviews, where there is no target item
|
||||
///
|
||||
/// A network call is triggered by a CommandExecution item, so having a
|
||||
/// target_item_id set to the CommandExecution item would be misleading
|
||||
/// because the review is about the network call, not the command execution.
|
||||
/// Therefore, target_item_id is set to None for network policy reviews.
|
||||
pub target_item_id: Option<String>,
|
||||
pub decision_source: AutoReviewDecisionSource,
|
||||
pub review: GuardianApprovalReview,
|
||||
pub action: GuardianApprovalReviewAction,
|
||||
}
|
||||
@@ -6258,24 +6273,6 @@ pub struct AccountRateLimitsUpdatedNotification {
|
||||
pub rate_limits: RateLimitSnapshot,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct AddCreditsNudgeEmailNotification {
|
||||
pub thread_id: String,
|
||||
pub result: AddCreditsNudgeEmailResult,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(tag = "status", rename_all = "camelCase")]
|
||||
#[ts(tag = "status", rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum AddCreditsNudgeEmailResult {
|
||||
Sent,
|
||||
CooldownActive,
|
||||
Failed { message: String },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -6285,7 +6282,6 @@ pub struct RateLimitSnapshot {
|
||||
pub primary: Option<RateLimitWindow>,
|
||||
pub secondary: Option<RateLimitWindow>,
|
||||
pub credits: Option<CreditsSnapshot>,
|
||||
pub spend_control: Option<SpendControlSnapshot>,
|
||||
pub plan_type: Option<PlanType>,
|
||||
}
|
||||
|
||||
@@ -6297,7 +6293,6 @@ impl From<CoreRateLimitSnapshot> for RateLimitSnapshot {
|
||||
primary: value.primary.map(RateLimitWindow::from),
|
||||
secondary: value.secondary.map(RateLimitWindow::from),
|
||||
credits: value.credits.map(CreditsSnapshot::from),
|
||||
spend_control: value.spend_control.map(SpendControlSnapshot::from),
|
||||
plan_type: value.plan_type,
|
||||
}
|
||||
}
|
||||
@@ -6343,21 +6338,6 @@ impl From<CoreCreditsSnapshot> for CreditsSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct SpendControlSnapshot {
|
||||
pub reached: bool,
|
||||
}
|
||||
|
||||
impl From<CoreSpendControlSnapshot> for SpendControlSnapshot {
|
||||
fn from(value: CoreSpendControlSnapshot) -> Self {
|
||||
Self {
|
||||
reached: value.reached,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
||||
@@ -133,7 +133,7 @@ Example with notification opt-out:
|
||||
|
||||
## API Overview
|
||||
|
||||
- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`.
|
||||
- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`.
|
||||
- `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it.
|
||||
- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; if the source thread is currently mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread.
|
||||
- `thread/list` — page through stored rollouts; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `cwd`, and `searchTerm` filters. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded.
|
||||
@@ -147,7 +147,6 @@ Example with notification opt-out:
|
||||
- `thread/unarchive` — move an archived rollout file back into the sessions directory; returns the restored `thread` on success and emits `thread/unarchived`.
|
||||
- `thread/compact/start` — trigger conversation history compaction for a thread; returns `{}` immediately while progress streams through standard turn/item notifications.
|
||||
- `thread/shellCommand` — run a user-initiated `!` shell command against a thread; this runs unsandboxed with full access rather than inheriting the thread sandbox policy. Returns `{}` immediately while progress streams through standard turn/item notifications and any active turn receives the formatted output in its message stream.
|
||||
- `thread/addCreditsNudgeEmail` — ask the backend to notify the workspace owner that the thread hit a usage limit; returns `{}` immediately and emits `account/addCreditsNudgeEmail/completed` with the result.
|
||||
- `thread/backgroundTerminals/clean` — terminate all running background terminals for a thread (experimental; requires `capabilities.experimentalApi`); returns `{}` when the cleanup request is accepted.
|
||||
- `thread/rollback` — drop the last N turns from the agent’s in-memory context and persist a rollback marker in the rollout so future resumes see the pruned history; returns the updated `thread` (with `turns` populated) on success.
|
||||
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode".
|
||||
@@ -213,6 +212,7 @@ Start a fresh thread when you need a new Codex conversation.
|
||||
"sandbox": "workspaceWrite",
|
||||
"personality": "friendly",
|
||||
"serviceName": "my_app_server_client", // optional metrics tag (`service_name`)
|
||||
"sessionStartSource": "startup", // optional: "startup" (default) or "clear"
|
||||
// Experimental: requires opt-in
|
||||
"dynamicTools": [
|
||||
{
|
||||
@@ -457,15 +457,6 @@ If the thread does not already have an active turn, the server starts a standalo
|
||||
{ "id": 26, "result": {} }
|
||||
```
|
||||
|
||||
### Example: Notify Workspace Owner About Usage Limit
|
||||
|
||||
Use `thread/addCreditsNudgeEmail` when a usage-limit prompt needs to notify the thread's workspace owner. The request returns immediately with `{}`. The final delivery result is emitted separately as `account/addCreditsNudgeEmail/completed` for the same `threadId`.
|
||||
|
||||
```json
|
||||
{ "method": "thread/addCreditsNudgeEmail", "id": 27, "params": { "threadId": "thr_b" } }
|
||||
{ "id": 27, "result": {} }
|
||||
```
|
||||
|
||||
### Example: Start a turn (send user input)
|
||||
|
||||
Turns attach user input (text or images) to a thread and trigger Codex generation. The `input` field is a list of discriminated unions:
|
||||
@@ -1366,19 +1357,19 @@ The JSON-RPC auth/account surface exposes request/response methods plus server-i
|
||||
|
||||
### Authentication modes
|
||||
|
||||
Codex supports these authentication modes. The current mode is surfaced in `account/updated` (`authMode`), which also includes the current ChatGPT `planType` when available, the current `workspaceRole` when live account metadata can be fetched, and the derived `isWorkspaceOwner` flag. The same cached account state can be read from `account/read`.
|
||||
Codex supports these authentication modes. The current mode is surfaced in `account/updated` (`authMode`), which also includes the current ChatGPT `planType` when available, and can be inferred from `account/read`.
|
||||
|
||||
- **API key (`apiKey`)**: Caller supplies an OpenAI API key via `account/login/start` with `type: "apiKey"`. The API key is saved and used for API requests.
|
||||
- **ChatGPT managed (`chatgpt`)** (recommended): Codex owns the ChatGPT OAuth flow and refresh tokens. Start via `account/login/start` with `type: "chatgpt"` for the browser flow or `type: "chatgptDeviceCode"` for device code; Codex persists tokens to disk and refreshes them automatically.
|
||||
|
||||
### API Overview
|
||||
|
||||
- `account/read` — fetch cached current account info; optionally refresh tokens. ChatGPT workspace role is refreshed in the background and delivered by `account/updated` so this request does not wait for live account metadata.
|
||||
- `account/read` — fetch current account info; optionally refresh tokens.
|
||||
- `account/login/start` — begin login (`apiKey`, `chatgpt`, `chatgptDeviceCode`).
|
||||
- `account/login/completed` (notify) — emitted when a login attempt finishes (success or error).
|
||||
- `account/login/cancel` — cancel a pending managed ChatGPT login by `loginId`.
|
||||
- `account/logout` — sign out; triggers `account/updated`.
|
||||
- `account/updated` (notify) — emitted whenever auth mode changes (`authMode`: `apikey`, `chatgpt`, or `null`) and includes the current ChatGPT `planType`, `workspaceRole`, and `isWorkspaceOwner`.
|
||||
- `account/updated` (notify) — emitted whenever auth mode changes (`authMode`: `apikey`, `chatgpt`, or `null`) and includes the current ChatGPT `planType` when available.
|
||||
- `account/rateLimits/read` — fetch ChatGPT rate limits; updates arrive via `account/rateLimits/updated` (notify).
|
||||
- `account/rateLimits/updated` (notify) — emitted whenever a user's ChatGPT rate limits change.
|
||||
- `mcpServer/oauthLogin/completed` (notify) — emitted after a `mcpServer/oauth/login` flow finishes for a server; payload includes `{ name, success, error? }`.
|
||||
@@ -1395,19 +1386,16 @@ Request:
|
||||
Response examples:
|
||||
|
||||
```json
|
||||
{ "id": 1, "result": { "account": null, "workspaceRole": null, "isWorkspaceOwner": null, "requiresOpenaiAuth": false } } // No OpenAI auth needed (e.g., OSS/local models)
|
||||
{ "id": 1, "result": { "account": null, "workspaceRole": null, "isWorkspaceOwner": null, "requiresOpenaiAuth": true } } // OpenAI auth required (typical for OpenAI-hosted models)
|
||||
{ "id": 1, "result": { "account": { "type": "apiKey" }, "workspaceRole": null, "isWorkspaceOwner": null, "requiresOpenaiAuth": true } }
|
||||
{ "id": 1, "result": { "account": { "type": "chatgpt", "email": "user@example.com", "planType": "pro" }, "workspaceRole": null, "isWorkspaceOwner": true, "requiresOpenaiAuth": true } }
|
||||
{ "method": "account/updated", "params": { "authMode": "chatgpt", "planType": "pro", "workspaceRole": "account-admin", "isWorkspaceOwner": true } } // emitted when live workspace metadata arrives
|
||||
{ "id": 1, "result": { "account": null, "requiresOpenaiAuth": false } } // No OpenAI auth needed (e.g., OSS/local models)
|
||||
{ "id": 1, "result": { "account": null, "requiresOpenaiAuth": true } } // OpenAI auth required (typical for OpenAI-hosted models)
|
||||
{ "id": 1, "result": { "account": { "type": "apiKey" }, "requiresOpenaiAuth": true } }
|
||||
{ "id": 1, "result": { "account": { "type": "chatgpt", "email": "user@example.com", "planType": "pro" }, "requiresOpenaiAuth": true } }
|
||||
```
|
||||
|
||||
Field notes:
|
||||
|
||||
- `refreshToken` (bool): set `true` to force a token refresh.
|
||||
- `requiresOpenaiAuth` reflects the active provider; when `false`, Codex can run without OpenAI credentials.
|
||||
- `workspaceRole` is a nullable live account role from ChatGPT account metadata: `account-owner`, `account-admin`, or `standard-user`. It is normally delivered asynchronously in `account/updated`; clients should treat `null` from `account/read` as "not known yet".
|
||||
- `isWorkspaceOwner` is a nullable convenience flag. When `workspaceRole` is available, owners and admins map to `true` and standard users map to `false`; otherwise the server may fall back to the managed-account token's workspace-owner claim.
|
||||
|
||||
### 2) Log in with an API key
|
||||
|
||||
@@ -1426,7 +1414,7 @@ Field notes:
|
||||
3. Notifications:
|
||||
```json
|
||||
{ "method": "account/login/completed", "params": { "loginId": null, "success": true, "error": null } }
|
||||
{ "method": "account/updated", "params": { "authMode": "apikey", "planType": null, "workspaceRole": null, "isWorkspaceOwner": null } }
|
||||
{ "method": "account/updated", "params": { "authMode": "apikey", "planType": null } }
|
||||
```
|
||||
|
||||
### 3) Log in with ChatGPT (browser flow)
|
||||
@@ -1440,7 +1428,7 @@ Field notes:
|
||||
3. Wait for notifications:
|
||||
```json
|
||||
{ "method": "account/login/completed", "params": { "loginId": "<uuid>", "success": true, "error": null } }
|
||||
{ "method": "account/updated", "params": { "authMode": "chatgpt", "planType": "plus", "workspaceRole": "account-owner", "isWorkspaceOwner": true } }
|
||||
{ "method": "account/updated", "params": { "authMode": "chatgpt", "planType": "plus" } }
|
||||
```
|
||||
|
||||
### 4) Log in with ChatGPT (device code flow)
|
||||
@@ -1454,7 +1442,7 @@ Field notes:
|
||||
3. Wait for notifications:
|
||||
```json
|
||||
{ "method": "account/login/completed", "params": { "loginId": "<uuid>", "success": true, "error": null } }
|
||||
{ "method": "account/updated", "params": { "authMode": "chatgpt", "planType": "plus", "workspaceRole": "account-owner", "isWorkspaceOwner": true } }
|
||||
{ "method": "account/updated", "params": { "authMode": "chatgpt", "planType": "plus" } }
|
||||
```
|
||||
|
||||
### 5) Cancel a ChatGPT login
|
||||
@@ -1469,7 +1457,7 @@ Field notes:
|
||||
```json
|
||||
{ "method": "account/logout", "id": 6 }
|
||||
{ "id": 6, "result": {} }
|
||||
{ "method": "account/updated", "params": { "authMode": null, "planType": null, "workspaceRole": null, "isWorkspaceOwner": null } }
|
||||
{ "method": "account/updated", "params": { "authMode": null, "planType": null } }
|
||||
```
|
||||
|
||||
### 7) Rate limits (ChatGPT)
|
||||
|
||||
@@ -13,8 +13,6 @@ use crate::thread_state::resolve_server_request_on_thread_listener;
|
||||
use crate::thread_status::ThreadWatchActiveGuard;
|
||||
use crate::thread_status::ThreadWatchManager;
|
||||
use codex_app_server_protocol::AccountRateLimitsUpdatedNotification;
|
||||
use codex_app_server_protocol::AddCreditsNudgeEmailNotification;
|
||||
use codex_app_server_protocol::AddCreditsNudgeEmailResult;
|
||||
use codex_app_server_protocol::AdditionalPermissionProfile as V2AdditionalPermissionProfile;
|
||||
use codex_app_server_protocol::AgentMessageDeltaNotification;
|
||||
use codex_app_server_protocol::ApplyPatchApprovalParams;
|
||||
@@ -119,7 +117,6 @@ use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynam
|
||||
use codex_protocol::dynamic_tools::DynamicToolResponse as CoreDynamicToolResponse;
|
||||
use codex_protocol::items::parse_hook_prompt_message;
|
||||
use codex_protocol::plan_tool::UpdatePlanArgs;
|
||||
use codex_protocol::protocol::AddCreditsNudgeEmailStatus as CoreAddCreditsNudgeEmailStatus;
|
||||
use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo;
|
||||
use codex_protocol::protocol::Event;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
@@ -226,26 +223,6 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
)
|
||||
.await;
|
||||
}
|
||||
EventMsg::AddCreditsNudgeEmailResponse(event) => {
|
||||
if let ApiVersion::V2 = api_version {
|
||||
let result = match event.result {
|
||||
Ok(CoreAddCreditsNudgeEmailStatus::Sent) => AddCreditsNudgeEmailResult::Sent,
|
||||
Ok(CoreAddCreditsNudgeEmailStatus::CooldownActive) => {
|
||||
AddCreditsNudgeEmailResult::CooldownActive
|
||||
}
|
||||
Err(message) => AddCreditsNudgeEmailResult::Failed { message },
|
||||
};
|
||||
let notification = AddCreditsNudgeEmailNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
result,
|
||||
};
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::AddCreditsNudgeEmailCompleted(
|
||||
notification,
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
EventMsg::SkillsUpdateAvailable => {
|
||||
if let ApiVersion::V2 = api_version {
|
||||
outgoing
|
||||
@@ -291,15 +268,19 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
CommandExecutionStatus::InProgress,
|
||||
) {
|
||||
Some(ThreadItem::CommandExecution {
|
||||
id,
|
||||
command,
|
||||
cwd,
|
||||
command_actions,
|
||||
..
|
||||
}) => Some(CommandExecutionCompletionItem {
|
||||
command,
|
||||
cwd,
|
||||
command_actions,
|
||||
}),
|
||||
}) => Some((
|
||||
id,
|
||||
CommandExecutionCompletionItem {
|
||||
command,
|
||||
cwd,
|
||||
command_actions,
|
||||
},
|
||||
)),
|
||||
Some(_) | None => None,
|
||||
};
|
||||
let assessment_turn_id = if assessment.turn_id.is_empty() {
|
||||
@@ -309,12 +290,13 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
};
|
||||
if assessment.status
|
||||
== codex_protocol::protocol::GuardianAssessmentStatus::InProgress
|
||||
&& let Some(completion_item) = pending_command_execution.as_ref()
|
||||
&& let Some((target_item_id, completion_item)) =
|
||||
pending_command_execution.as_ref()
|
||||
{
|
||||
start_command_execution_item(
|
||||
&conversation_id,
|
||||
assessment_turn_id.clone(),
|
||||
assessment.id.clone(),
|
||||
target_item_id.clone(),
|
||||
completion_item.command.clone(),
|
||||
completion_item.cwd.clone(),
|
||||
completion_item.command_actions.clone(),
|
||||
@@ -334,12 +316,12 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
assessment.status,
|
||||
codex_protocol::protocol::GuardianAssessmentStatus::Denied
|
||||
| codex_protocol::protocol::GuardianAssessmentStatus::Aborted
|
||||
) && let Some(completion_item) = pending_command_execution
|
||||
) && let Some((target_item_id, completion_item)) = pending_command_execution
|
||||
{
|
||||
complete_command_execution_item(
|
||||
&conversation_id,
|
||||
assessment_turn_id,
|
||||
assessment.id.clone(),
|
||||
target_item_id,
|
||||
completion_item.command,
|
||||
completion_item.cwd,
|
||||
/*process_id*/ None,
|
||||
@@ -2919,6 +2901,7 @@ mod tests {
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use anyhow::bail;
|
||||
use codex_app_server_protocol::AutoReviewDecisionSource;
|
||||
use codex_app_server_protocol::GuardianApprovalReviewStatus;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::TurnPlanStepStatus;
|
||||
@@ -3020,12 +3003,18 @@ mod tests {
|
||||
GuardianAssessmentStatus::Aborted => (None, None, None),
|
||||
};
|
||||
GuardianAssessmentEvent {
|
||||
id: id.to_string(),
|
||||
id: format!("review-{id}"),
|
||||
target_item_id: Some(id.to_string()),
|
||||
turn_id: turn_id.to_string(),
|
||||
status,
|
||||
risk_level,
|
||||
user_authorization,
|
||||
rationale,
|
||||
decision_source: if matches!(status, GuardianAssessmentStatus::InProgress) {
|
||||
None
|
||||
} else {
|
||||
Some(codex_protocol::protocol::GuardianAssessmentDecisionSource::Agent)
|
||||
},
|
||||
action: serde_json::from_value(json!({
|
||||
"type": "command",
|
||||
"source": "shell",
|
||||
@@ -3080,12 +3069,14 @@ mod tests {
|
||||
&conversation_id,
|
||||
"turn-from-event",
|
||||
&GuardianAssessmentEvent {
|
||||
id: "item-1".to_string(),
|
||||
id: "review-1".to_string(),
|
||||
target_item_id: Some("item-1".to_string()),
|
||||
turn_id: String::new(),
|
||||
status: codex_protocol::protocol::GuardianAssessmentStatus::InProgress,
|
||||
risk_level: None,
|
||||
user_authorization: None,
|
||||
rationale: None,
|
||||
decision_source: None,
|
||||
action: action.clone(),
|
||||
},
|
||||
);
|
||||
@@ -3094,7 +3085,8 @@ mod tests {
|
||||
ServerNotification::ItemGuardianApprovalReviewStarted(payload) => {
|
||||
assert_eq!(payload.thread_id, conversation_id.to_string());
|
||||
assert_eq!(payload.turn_id, "turn-from-event");
|
||||
assert_eq!(payload.target_item_id, "item-1");
|
||||
assert_eq!(payload.review_id, "review-1");
|
||||
assert_eq!(payload.target_item_id.as_deref(), Some("item-1"));
|
||||
assert_eq!(
|
||||
payload.review.status,
|
||||
GuardianApprovalReviewStatus::InProgress
|
||||
@@ -3120,12 +3112,16 @@ mod tests {
|
||||
&conversation_id,
|
||||
"turn-from-event",
|
||||
&GuardianAssessmentEvent {
|
||||
id: "item-2".to_string(),
|
||||
id: "review-2".to_string(),
|
||||
target_item_id: Some("item-2".to_string()),
|
||||
turn_id: "turn-from-assessment".to_string(),
|
||||
status: codex_protocol::protocol::GuardianAssessmentStatus::Denied,
|
||||
risk_level: Some(codex_protocol::protocol::GuardianRiskLevel::High),
|
||||
user_authorization: Some(codex_protocol::protocol::GuardianUserAuthorization::Low),
|
||||
rationale: Some("too risky".to_string()),
|
||||
decision_source: Some(
|
||||
codex_protocol::protocol::GuardianAssessmentDecisionSource::Agent,
|
||||
),
|
||||
action: action.clone(),
|
||||
},
|
||||
);
|
||||
@@ -3134,7 +3130,9 @@ mod tests {
|
||||
ServerNotification::ItemGuardianApprovalReviewCompleted(payload) => {
|
||||
assert_eq!(payload.thread_id, conversation_id.to_string());
|
||||
assert_eq!(payload.turn_id, "turn-from-assessment");
|
||||
assert_eq!(payload.target_item_id, "item-2");
|
||||
assert_eq!(payload.review_id, "review-2");
|
||||
assert_eq!(payload.target_item_id.as_deref(), Some("item-2"));
|
||||
assert_eq!(payload.decision_source, AutoReviewDecisionSource::Agent);
|
||||
assert_eq!(payload.review.status, GuardianApprovalReviewStatus::Denied);
|
||||
assert_eq!(
|
||||
payload.review.risk_level,
|
||||
@@ -3164,12 +3162,16 @@ mod tests {
|
||||
&conversation_id,
|
||||
"turn-from-event",
|
||||
&GuardianAssessmentEvent {
|
||||
id: "item-3".to_string(),
|
||||
id: "review-3".to_string(),
|
||||
target_item_id: None,
|
||||
turn_id: "turn-from-assessment".to_string(),
|
||||
status: codex_protocol::protocol::GuardianAssessmentStatus::Aborted,
|
||||
risk_level: None,
|
||||
user_authorization: None,
|
||||
rationale: None,
|
||||
decision_source: Some(
|
||||
codex_protocol::protocol::GuardianAssessmentDecisionSource::Agent,
|
||||
),
|
||||
action: action.clone(),
|
||||
},
|
||||
);
|
||||
@@ -3178,7 +3180,9 @@ mod tests {
|
||||
ServerNotification::ItemGuardianApprovalReviewCompleted(payload) => {
|
||||
assert_eq!(payload.thread_id, conversation_id.to_string());
|
||||
assert_eq!(payload.turn_id, "turn-from-assessment");
|
||||
assert_eq!(payload.target_item_id, "item-3");
|
||||
assert_eq!(payload.review_id, "review-3");
|
||||
assert_eq!(payload.target_item_id, None);
|
||||
assert_eq!(payload.decision_source, AutoReviewDecisionSource::Agent);
|
||||
assert_eq!(payload.review.status, GuardianApprovalReviewStatus::Aborted);
|
||||
assert_eq!(payload.review.risk_level, None);
|
||||
assert_eq!(payload.review.user_authorization, None);
|
||||
@@ -3395,7 +3399,11 @@ mod tests {
|
||||
OutgoingMessage::AppServerNotification(
|
||||
ServerNotification::ItemGuardianApprovalReviewStarted(payload),
|
||||
) => {
|
||||
assert_eq!(payload.target_item_id, "cmd-guardian-approved");
|
||||
assert_eq!(payload.review_id, "review-cmd-guardian-approved");
|
||||
assert_eq!(
|
||||
payload.target_item_id.as_deref(),
|
||||
Some("cmd-guardian-approved")
|
||||
);
|
||||
assert_eq!(
|
||||
payload.review.status,
|
||||
GuardianApprovalReviewStatus::InProgress
|
||||
@@ -3416,7 +3424,12 @@ mod tests {
|
||||
OutgoingMessage::AppServerNotification(
|
||||
ServerNotification::ItemGuardianApprovalReviewCompleted(payload),
|
||||
) => {
|
||||
assert_eq!(payload.target_item_id, "cmd-guardian-approved");
|
||||
assert_eq!(payload.review_id, "review-cmd-guardian-approved");
|
||||
assert_eq!(
|
||||
payload.target_item_id.as_deref(),
|
||||
Some("cmd-guardian-approved")
|
||||
);
|
||||
assert_eq!(payload.decision_source, AutoReviewDecisionSource::Agent);
|
||||
assert_eq!(
|
||||
payload.review.status,
|
||||
GuardianApprovalReviewStatus::Approved
|
||||
@@ -3453,7 +3466,11 @@ mod tests {
|
||||
OutgoingMessage::AppServerNotification(
|
||||
ServerNotification::ItemGuardianApprovalReviewStarted(payload),
|
||||
) => {
|
||||
assert_eq!(payload.target_item_id, "cmd-guardian-denied");
|
||||
assert_eq!(payload.review_id, "review-cmd-guardian-denied");
|
||||
assert_eq!(
|
||||
payload.target_item_id.as_deref(),
|
||||
Some("cmd-guardian-denied")
|
||||
);
|
||||
assert_eq!(
|
||||
payload.review.status,
|
||||
GuardianApprovalReviewStatus::InProgress
|
||||
@@ -3474,7 +3491,12 @@ mod tests {
|
||||
OutgoingMessage::AppServerNotification(
|
||||
ServerNotification::ItemGuardianApprovalReviewCompleted(payload),
|
||||
) => {
|
||||
assert_eq!(payload.target_item_id, "cmd-guardian-denied");
|
||||
assert_eq!(payload.review_id, "review-cmd-guardian-denied");
|
||||
assert_eq!(
|
||||
payload.target_item_id.as_deref(),
|
||||
Some("cmd-guardian-denied")
|
||||
);
|
||||
assert_eq!(payload.decision_source, AutoReviewDecisionSource::Agent);
|
||||
assert_eq!(payload.review.status, GuardianApprovalReviewStatus::Denied);
|
||||
}
|
||||
other => bail!("unexpected message: {other:?}"),
|
||||
@@ -3491,6 +3513,30 @@ mod tests {
|
||||
other => bail!("unexpected message: {other:?}"),
|
||||
}
|
||||
|
||||
let mut missing_target = guardian_command_assessment(
|
||||
"cmd-guardian-missing-target",
|
||||
"turn-guardian-missing-target",
|
||||
GuardianAssessmentStatus::InProgress,
|
||||
);
|
||||
missing_target.target_item_id = None;
|
||||
guardian_context
|
||||
.apply_guardian_assessment_event(missing_target)
|
||||
.await;
|
||||
let eighth = recv_broadcast_message(&mut rx).await?;
|
||||
match eighth {
|
||||
OutgoingMessage::AppServerNotification(
|
||||
ServerNotification::ItemGuardianApprovalReviewStarted(payload),
|
||||
) => {
|
||||
assert_eq!(payload.review_id, "review-cmd-guardian-missing-target");
|
||||
assert_eq!(payload.target_item_id, None);
|
||||
assert_eq!(
|
||||
payload.review.status,
|
||||
GuardianApprovalReviewStatus::InProgress
|
||||
);
|
||||
}
|
||||
other => bail!("unexpected message: {other:?}"),
|
||||
}
|
||||
|
||||
assert!(rx.try_recv().is_err(), "no extra messages expected");
|
||||
conversation.shutdown_and_wait().await?;
|
||||
Ok(())
|
||||
@@ -3993,7 +4039,6 @@ mod tests {
|
||||
unlimited: false,
|
||||
balance: Some("5".to_string()),
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
};
|
||||
|
||||
|
||||
@@ -116,8 +116,6 @@ use codex_app_server_protocol::SkillsConfigWriteResponse;
|
||||
use codex_app_server_protocol::SkillsListParams;
|
||||
use codex_app_server_protocol::SkillsListResponse;
|
||||
use codex_app_server_protocol::Thread;
|
||||
use codex_app_server_protocol::ThreadAddCreditsNudgeEmailParams;
|
||||
use codex_app_server_protocol::ThreadAddCreditsNudgeEmailResponse;
|
||||
use codex_app_server_protocol::ThreadArchiveParams;
|
||||
use codex_app_server_protocol::ThreadArchiveResponse;
|
||||
use codex_app_server_protocol::ThreadArchivedNotification;
|
||||
@@ -186,7 +184,6 @@ use codex_app_server_protocol::WindowsSandboxSetupCompletedNotification;
|
||||
use codex_app_server_protocol::WindowsSandboxSetupMode;
|
||||
use codex_app_server_protocol::WindowsSandboxSetupStartParams;
|
||||
use codex_app_server_protocol::WindowsSandboxSetupStartResponse;
|
||||
use codex_app_server_protocol::WorkspaceRole;
|
||||
use codex_app_server_protocol::build_turns_from_rollout_items;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_backend_client::Client as BackendClient;
|
||||
@@ -203,7 +200,6 @@ use codex_core::SteerInputError;
|
||||
use codex_core::ThreadConfigSnapshot;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::ThreadSortKey as CoreThreadSortKey;
|
||||
use codex_core::WorkspaceRole as CoreWorkspaceRole;
|
||||
use codex_core::append_thread_name;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
@@ -490,96 +486,6 @@ pub(crate) struct CodexMessageProcessorArgs {
|
||||
pub(crate) log_db: Option<LogDbLayer>,
|
||||
}
|
||||
|
||||
async fn resolve_workspace_role_and_owner_for_auth(
|
||||
chatgpt_base_url: &str,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> (Option<WorkspaceRole>, Option<bool>) {
|
||||
let ownership =
|
||||
codex_core::resolve_workspace_role_and_owner_for_auth(chatgpt_base_url, auth).await;
|
||||
(
|
||||
ownership.workspace_role.map(workspace_role_to_v2),
|
||||
ownership.is_workspace_owner,
|
||||
)
|
||||
}
|
||||
|
||||
fn workspace_role_to_v2(role: CoreWorkspaceRole) -> WorkspaceRole {
|
||||
match role {
|
||||
CoreWorkspaceRole::AccountOwner => WorkspaceRole::AccountOwner,
|
||||
CoreWorkspaceRole::AccountAdmin => WorkspaceRole::AccountAdmin,
|
||||
CoreWorkspaceRole::StandardUser => WorkspaceRole::StandardUser,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct AuthIdentity {
|
||||
auth_mode: AuthMode,
|
||||
account_id: Option<String>,
|
||||
account_email: Option<String>,
|
||||
chatgpt_user_id: Option<String>,
|
||||
}
|
||||
|
||||
fn auth_identity(auth: &CodexAuth) -> AuthIdentity {
|
||||
AuthIdentity {
|
||||
auth_mode: auth.api_auth_mode(),
|
||||
account_id: auth.get_account_id(),
|
||||
account_email: auth.get_account_email(),
|
||||
chatgpt_user_id: auth.get_chatgpt_user_id(),
|
||||
}
|
||||
}
|
||||
|
||||
fn cached_workspace_role_and_owner_for_auth(
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> (Option<WorkspaceRole>, Option<bool>) {
|
||||
(None, auth.and_then(CodexAuth::is_workspace_owner))
|
||||
}
|
||||
|
||||
fn account_updated_notification_for_auth(
|
||||
auth: Option<&CodexAuth>,
|
||||
workspace_role: Option<WorkspaceRole>,
|
||||
is_workspace_owner: Option<bool>,
|
||||
) -> AccountUpdatedNotification {
|
||||
AccountUpdatedNotification {
|
||||
auth_mode: auth.map(CodexAuth::api_auth_mode),
|
||||
plan_type: auth.and_then(CodexAuth::account_plan_type),
|
||||
workspace_role,
|
||||
is_workspace_owner,
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_live_workspace_role_update_for_auth(
|
||||
outgoing: Arc<OutgoingMessageSender>,
|
||||
auth_manager: Arc<AuthManager>,
|
||||
chatgpt_base_url: String,
|
||||
auth: Option<CodexAuth>,
|
||||
) {
|
||||
let Some(auth) = auth.filter(CodexAuth::is_chatgpt_auth) else {
|
||||
return;
|
||||
};
|
||||
let expected_identity = auth_identity(&auth);
|
||||
|
||||
tokio::spawn(async move {
|
||||
let (workspace_role, is_workspace_owner) =
|
||||
resolve_workspace_role_and_owner_for_auth(&chatgpt_base_url, Some(&auth)).await;
|
||||
let Some(workspace_role) = workspace_role else {
|
||||
return;
|
||||
};
|
||||
|
||||
let current_auth = auth_manager.auth_cached();
|
||||
if current_auth.as_ref().map(auth_identity) != Some(expected_identity) {
|
||||
return;
|
||||
}
|
||||
|
||||
let payload = account_updated_notification_for_auth(
|
||||
current_auth.as_ref(),
|
||||
Some(workspace_role),
|
||||
is_workspace_owner,
|
||||
);
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::AccountUpdated(payload))
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
impl CodexMessageProcessor {
|
||||
pub(crate) fn handle_config_mutation(&self) {
|
||||
self.clear_plugin_related_caches();
|
||||
@@ -592,18 +498,10 @@ impl CodexMessageProcessor {
|
||||
|
||||
fn current_account_updated_notification(&self) -> AccountUpdatedNotification {
|
||||
let auth = self.auth_manager.auth_cached();
|
||||
let (workspace_role, is_workspace_owner) =
|
||||
cached_workspace_role_and_owner_for_auth(auth.as_ref());
|
||||
account_updated_notification_for_auth(auth.as_ref(), workspace_role, is_workspace_owner)
|
||||
}
|
||||
|
||||
fn spawn_live_workspace_role_update(&self, auth: Option<CodexAuth>) {
|
||||
spawn_live_workspace_role_update_for_auth(
|
||||
self.outgoing.clone(),
|
||||
self.auth_manager.clone(),
|
||||
self.config.chatgpt_base_url.clone(),
|
||||
auth,
|
||||
);
|
||||
AccountUpdatedNotification {
|
||||
auth_mode: auth.as_ref().map(CodexAuth::api_auth_mode),
|
||||
plan_type: auth.as_ref().and_then(CodexAuth::account_plan_type),
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_thread(
|
||||
@@ -887,10 +785,6 @@ impl CodexMessageProcessor {
|
||||
self.thread_shell_command(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::ThreadAddCreditsNudgeEmail { request_id, params } => {
|
||||
self.thread_add_credits_nudge_email(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::SkillsList { request_id, params } => {
|
||||
self.skills_list(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
@@ -1210,7 +1104,6 @@ impl CodexMessageProcessor {
|
||||
self.current_account_updated_notification(),
|
||||
))
|
||||
.await;
|
||||
self.spawn_live_workspace_role_update(self.auth_manager.auth_cached());
|
||||
}
|
||||
Err(error) => {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
@@ -1335,7 +1228,7 @@ impl CodexMessageProcessor {
|
||||
replace_cloud_requirements_loader(
|
||||
cloud_requirements.as_ref(),
|
||||
auth_manager.clone(),
|
||||
chatgpt_base_url.clone(),
|
||||
chatgpt_base_url,
|
||||
codex_home,
|
||||
);
|
||||
sync_default_client_residency_requirement(
|
||||
@@ -1344,27 +1237,17 @@ impl CodexMessageProcessor {
|
||||
)
|
||||
.await;
|
||||
|
||||
// Notify clients with the actual current auth mode immediately; the
|
||||
// live workspace role is fetched below without delaying this update.
|
||||
// Notify clients with the actual current auth mode.
|
||||
let auth = auth_manager.auth_cached();
|
||||
let (workspace_role, is_workspace_owner) =
|
||||
cached_workspace_role_and_owner_for_auth(auth.as_ref());
|
||||
let payload_v2 = account_updated_notification_for_auth(
|
||||
auth.as_ref(),
|
||||
workspace_role,
|
||||
is_workspace_owner,
|
||||
);
|
||||
let payload_v2 = AccountUpdatedNotification {
|
||||
auth_mode: auth.as_ref().map(CodexAuth::api_auth_mode),
|
||||
plan_type: auth.as_ref().and_then(CodexAuth::account_plan_type),
|
||||
};
|
||||
outgoing_clone
|
||||
.send_server_notification(ServerNotification::AccountUpdated(
|
||||
payload_v2,
|
||||
))
|
||||
.await;
|
||||
spawn_live_workspace_role_update_for_auth(
|
||||
outgoing_clone.clone(),
|
||||
auth_manager.clone(),
|
||||
chatgpt_base_url.clone(),
|
||||
auth,
|
||||
);
|
||||
}
|
||||
|
||||
// Clear the active login if it matches this attempt. It may have been replaced or cancelled.
|
||||
@@ -1459,7 +1342,7 @@ impl CodexMessageProcessor {
|
||||
replace_cloud_requirements_loader(
|
||||
cloud_requirements.as_ref(),
|
||||
auth_manager.clone(),
|
||||
chatgpt_base_url.clone(),
|
||||
chatgpt_base_url,
|
||||
codex_home,
|
||||
);
|
||||
sync_default_client_residency_requirement(
|
||||
@@ -1469,24 +1352,15 @@ impl CodexMessageProcessor {
|
||||
.await;
|
||||
|
||||
let auth = auth_manager.auth_cached();
|
||||
let (workspace_role, is_workspace_owner) =
|
||||
cached_workspace_role_and_owner_for_auth(auth.as_ref());
|
||||
let payload_v2 = account_updated_notification_for_auth(
|
||||
auth.as_ref(),
|
||||
workspace_role,
|
||||
is_workspace_owner,
|
||||
);
|
||||
let payload_v2 = AccountUpdatedNotification {
|
||||
auth_mode: auth.as_ref().map(CodexAuth::api_auth_mode),
|
||||
plan_type: auth.as_ref().and_then(CodexAuth::account_plan_type),
|
||||
};
|
||||
outgoing_clone
|
||||
.send_server_notification(ServerNotification::AccountUpdated(
|
||||
payload_v2,
|
||||
))
|
||||
.await;
|
||||
spawn_live_workspace_role_update_for_auth(
|
||||
outgoing_clone.clone(),
|
||||
auth_manager.clone(),
|
||||
chatgpt_base_url.clone(),
|
||||
auth,
|
||||
);
|
||||
}
|
||||
|
||||
let mut guard = active_login.lock().await;
|
||||
@@ -1635,7 +1509,6 @@ impl CodexMessageProcessor {
|
||||
self.current_account_updated_notification(),
|
||||
))
|
||||
.await;
|
||||
self.spawn_live_workspace_role_update(self.auth_manager.auth_cached());
|
||||
}
|
||||
|
||||
async fn logout_common(&self) -> std::result::Result<Option<AuthMode>, JSONRPCErrorError> {
|
||||
@@ -1673,8 +1546,6 @@ impl CodexMessageProcessor {
|
||||
let payload_v2 = AccountUpdatedNotification {
|
||||
auth_mode: current_auth_method,
|
||||
plan_type: None,
|
||||
workspace_role: None,
|
||||
is_workspace_owner: None,
|
||||
};
|
||||
self.outgoing
|
||||
.send_server_notification(ServerNotification::AccountUpdated(payload_v2))
|
||||
@@ -1773,16 +1644,13 @@ impl CodexMessageProcessor {
|
||||
if !requires_openai_auth {
|
||||
let response = GetAccountResponse {
|
||||
account: None,
|
||||
workspace_role: None,
|
||||
is_workspace_owner: None,
|
||||
requires_openai_auth,
|
||||
};
|
||||
self.outgoing.send_response(request_id, response).await;
|
||||
return;
|
||||
}
|
||||
|
||||
let auth = self.auth_manager.auth_cached();
|
||||
let account = match auth.as_ref() {
|
||||
let account = match self.auth_manager.auth_cached() {
|
||||
Some(auth) => match auth.auth_mode() {
|
||||
CoreAuthMode::ApiKey => Some(Account::ApiKey {}),
|
||||
CoreAuthMode::Chatgpt | CoreAuthMode::ChatgptAuthTokens => {
|
||||
@@ -1809,17 +1677,12 @@ impl CodexMessageProcessor {
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let (workspace_role, is_workspace_owner) =
|
||||
cached_workspace_role_and_owner_for_auth(auth.as_ref());
|
||||
|
||||
let response = GetAccountResponse {
|
||||
account,
|
||||
workspace_role,
|
||||
is_workspace_owner,
|
||||
requires_openai_auth,
|
||||
};
|
||||
self.outgoing.send_response(request_id, response).await;
|
||||
self.spawn_live_workspace_role_update(auth);
|
||||
}
|
||||
|
||||
async fn get_account_rate_limits(&self, request_id: ConnectionRequestId) {
|
||||
@@ -1837,11 +1700,6 @@ impl CodexMessageProcessor {
|
||||
self.outgoing.send_response(request_id, response).await;
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
?request_id,
|
||||
error = %error.message,
|
||||
"account/rateLimits/read request failed"
|
||||
);
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
}
|
||||
}
|
||||
@@ -2236,6 +2094,7 @@ impl CodexMessageProcessor {
|
||||
experimental_raw_events,
|
||||
personality,
|
||||
ephemeral,
|
||||
session_start_source,
|
||||
persist_extended_history,
|
||||
} = params;
|
||||
let mut typesafe_overrides = self.build_thread_config_overrides(
|
||||
@@ -2277,6 +2136,7 @@ impl CodexMessageProcessor {
|
||||
config,
|
||||
typesafe_overrides,
|
||||
dynamic_tools,
|
||||
session_start_source,
|
||||
persist_extended_history,
|
||||
service_name,
|
||||
experimental_raw_events,
|
||||
@@ -2352,6 +2212,7 @@ impl CodexMessageProcessor {
|
||||
config_overrides: Option<HashMap<String, serde_json::Value>>,
|
||||
typesafe_overrides: ConfigOverrides,
|
||||
dynamic_tools: Option<Vec<ApiDynamicToolSpec>>,
|
||||
session_start_source: Option<codex_app_server_protocol::ThreadStartSource>,
|
||||
persist_extended_history: bool,
|
||||
service_name: Option<String>,
|
||||
experimental_raw_events: bool,
|
||||
@@ -2475,6 +2336,12 @@ impl CodexMessageProcessor {
|
||||
.thread_manager
|
||||
.start_thread_with_tools_and_service_name(
|
||||
config,
|
||||
match session_start_source
|
||||
.unwrap_or(codex_app_server_protocol::ThreadStartSource::Startup)
|
||||
{
|
||||
codex_app_server_protocol::ThreadStartSource::Startup => InitialHistory::New,
|
||||
codex_app_server_protocol::ThreadStartSource::Clear => InitialHistory::Cleared,
|
||||
},
|
||||
core_dynamic_tools,
|
||||
persist_extended_history,
|
||||
service_name,
|
||||
@@ -3542,40 +3409,6 @@ impl CodexMessageProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
async fn thread_add_credits_nudge_email(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ThreadAddCreditsNudgeEmailParams,
|
||||
) {
|
||||
let ThreadAddCreditsNudgeEmailParams { thread_id } = params;
|
||||
|
||||
let (_, thread) = match self.load_thread(&thread_id).await {
|
||||
Ok(v) => v,
|
||||
Err(error) => {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match self
|
||||
.submit_core_op(&request_id, thread.as_ref(), Op::SendAddCreditsNudgeEmail)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
self.outgoing
|
||||
.send_response(request_id, ThreadAddCreditsNudgeEmailResponse {})
|
||||
.await;
|
||||
}
|
||||
Err(err) => {
|
||||
self.send_internal_error(
|
||||
request_id,
|
||||
format!("failed to request add-credits nudge email: {err}"),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn thread_list(&self, request_id: ConnectionRequestId, params: ThreadListParams) {
|
||||
let ThreadListParams {
|
||||
cursor,
|
||||
@@ -4443,7 +4276,7 @@ impl CodexMessageProcessor {
|
||||
thread.preview = preview_from_rollout_items(items);
|
||||
Ok(thread)
|
||||
}
|
||||
InitialHistory::New => Err(format!(
|
||||
InitialHistory::New | InitialHistory::Cleared => Err(format!(
|
||||
"failed to build resume response for thread {thread_id}: initial history missing"
|
||||
)),
|
||||
};
|
||||
@@ -9114,7 +8947,7 @@ pub(crate) async fn read_rollout_items_from_rollout(
|
||||
path: &Path,
|
||||
) -> std::io::Result<Vec<RolloutItem>> {
|
||||
let items = match RolloutRecorder::get_rollout_history(path).await? {
|
||||
InitialHistory::New => Vec::new(),
|
||||
InitialHistory::New | InitialHistory::Cleared => Vec::new(),
|
||||
InitialHistory::Forked(items) => items,
|
||||
InitialHistory::Resumed(resumed) => resumed.history,
|
||||
};
|
||||
|
||||
@@ -735,7 +735,6 @@ mod tests {
|
||||
}),
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: Some(PlanType::Plus),
|
||||
},
|
||||
});
|
||||
@@ -755,7 +754,6 @@ mod tests {
|
||||
},
|
||||
"secondary": null,
|
||||
"credits": null,
|
||||
"spendControl": null,
|
||||
"planType": "plus"
|
||||
}
|
||||
},
|
||||
@@ -771,8 +769,6 @@ mod tests {
|
||||
let notification = ServerNotification::AccountUpdated(AccountUpdatedNotification {
|
||||
auth_mode: Some(AuthMode::ApiKey),
|
||||
plan_type: None,
|
||||
workspace_role: None,
|
||||
is_workspace_owner: None,
|
||||
});
|
||||
|
||||
let jsonrpc_notification = OutgoingMessage::AppServerNotification(notification);
|
||||
@@ -781,9 +777,7 @@ mod tests {
|
||||
"method": "account/updated",
|
||||
"params": {
|
||||
"authMode": "apikey",
|
||||
"planType": null,
|
||||
"workspaceRole": null,
|
||||
"isWorkspaceOwner": null
|
||||
"planType": null
|
||||
},
|
||||
}),
|
||||
serde_json::to_value(jsonrpc_notification)
|
||||
|
||||
@@ -60,11 +60,6 @@ impl ChatGptAuthFixture {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_org_owner(mut self, is_org_owner: bool) -> Self {
|
||||
self.claims.is_org_owner = Some(is_org_owner);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn email(mut self, email: impl Into<String>) -> Self {
|
||||
self.claims.email = Some(email.into());
|
||||
self
|
||||
@@ -87,7 +82,6 @@ pub struct ChatGptIdTokenClaims {
|
||||
pub plan_type: Option<String>,
|
||||
pub chatgpt_user_id: Option<String>,
|
||||
pub chatgpt_account_id: Option<String>,
|
||||
pub is_org_owner: Option<bool>,
|
||||
}
|
||||
|
||||
impl ChatGptIdTokenClaims {
|
||||
@@ -114,11 +108,6 @@ impl ChatGptIdTokenClaims {
|
||||
self.chatgpt_account_id = Some(chatgpt_account_id.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_org_owner(mut self, is_org_owner: bool) -> Self {
|
||||
self.is_org_owner = Some(is_org_owner);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode_id_token(claims: &ChatGptIdTokenClaims) -> Result<String> {
|
||||
@@ -137,9 +126,6 @@ pub fn encode_id_token(claims: &ChatGptIdTokenClaims) -> Result<String> {
|
||||
if let Some(chatgpt_account_id) = &claims.chatgpt_account_id {
|
||||
auth_payload.insert("chatgpt_account_id".to_string(), json!(chatgpt_account_id));
|
||||
}
|
||||
if let Some(is_org_owner) = claims.is_org_owner {
|
||||
auth_payload.insert("is_org_owner".to_string(), json!(is_org_owner));
|
||||
}
|
||||
if !auth_payload.is_empty() {
|
||||
payload.insert(
|
||||
"https://api.openai.com/auth".to_string(),
|
||||
|
||||
@@ -58,7 +58,6 @@ use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ReviewStartParams;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::SkillsListParams;
|
||||
use codex_app_server_protocol::ThreadAddCreditsNudgeEmailParams;
|
||||
use codex_app_server_protocol::ThreadArchiveParams;
|
||||
use codex_app_server_protocol::ThreadCompactStartParams;
|
||||
use codex_app_server_protocol::ThreadForkParams;
|
||||
@@ -415,16 +414,6 @@ impl McpProcess {
|
||||
self.send_request("thread/shellCommand", params).await
|
||||
}
|
||||
|
||||
/// Send a `thread/addCreditsNudgeEmail` JSON-RPC request.
|
||||
pub async fn send_thread_add_credits_nudge_email_request(
|
||||
&mut self,
|
||||
params: ThreadAddCreditsNudgeEmailParams,
|
||||
) -> anyhow::Result<i64> {
|
||||
let params = Some(serde_json::to_value(params)?);
|
||||
self.send_request("thread/addCreditsNudgeEmail", params)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Send a `thread/rollback` JSON-RPC request.
|
||||
pub async fn send_thread_rollback_request(
|
||||
&mut self,
|
||||
|
||||
@@ -28,7 +28,6 @@ use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::TurnCompletedNotification;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_app_server_protocol::WorkspaceRole;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_login::login_with_api_key;
|
||||
use codex_protocol::account::PlanType as AccountPlanType;
|
||||
@@ -56,7 +55,6 @@ struct CreateConfigTomlParams {
|
||||
forced_workspace_id: Option<String>,
|
||||
requires_openai_auth: Option<bool>,
|
||||
base_url: Option<String>,
|
||||
chatgpt_base_url: Option<String>,
|
||||
}
|
||||
|
||||
fn create_config_toml(codex_home: &Path, params: CreateConfigTomlParams) -> std::io::Result<()> {
|
||||
@@ -64,9 +62,6 @@ fn create_config_toml(codex_home: &Path, params: CreateConfigTomlParams) -> std:
|
||||
let base_url = params
|
||||
.base_url
|
||||
.unwrap_or_else(|| "http://127.0.0.1:0/v1".to_string());
|
||||
let chatgpt_base_url = params
|
||||
.chatgpt_base_url
|
||||
.unwrap_or_else(|| "http://127.0.0.1:0/backend-api".to_string());
|
||||
let forced_line = if let Some(method) = params.forced_method {
|
||||
format!("forced_login_method = \"{method}\"\n")
|
||||
} else {
|
||||
@@ -87,7 +82,6 @@ fn create_config_toml(codex_home: &Path, params: CreateConfigTomlParams) -> std:
|
||||
model = "mock-model"
|
||||
approval_policy = "never"
|
||||
sandbox_mode = "danger-full-access"
|
||||
chatgpt_base_url = "{chatgpt_base_url}"
|
||||
{forced_line}
|
||||
{forced_workspace_line}
|
||||
|
||||
@@ -128,49 +122,6 @@ async fn mock_device_code_usercode_failure(server: &MockServer, status: u16) {
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn mock_accounts_check_role(server: &MockServer, account_id: &str, role: &str) {
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/backend-api/accounts/check/v4-2023-04-27"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"accounts": {
|
||||
account_id: {
|
||||
"account": {
|
||||
"account_user_role": role,
|
||||
}
|
||||
}
|
||||
},
|
||||
"account_ordering": [account_id],
|
||||
})))
|
||||
.mount(server)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn mock_slow_accounts_check_role(
|
||||
server: &MockServer,
|
||||
account_id: &str,
|
||||
role: &str,
|
||||
delay: Duration,
|
||||
) {
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/backend-api/accounts/check/v4-2023-04-27"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_delay(delay)
|
||||
.set_body_json(json!({
|
||||
"accounts": {
|
||||
account_id: {
|
||||
"account": {
|
||||
"account_user_role": role,
|
||||
}
|
||||
}
|
||||
},
|
||||
"account_ordering": [account_id],
|
||||
})),
|
||||
)
|
||||
.mount(server)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn mock_device_code_token_success(server: &MockServer) {
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/accounts/deviceauth/token"))
|
||||
@@ -270,12 +221,10 @@ async fn set_auth_token_updates_account_and_notifies() -> Result<()> {
|
||||
CreateConfigTomlParams {
|
||||
requires_openai_auth: Some(true),
|
||||
base_url: Some(format!("{}/v1", mock_server.uri())),
|
||||
chatgpt_base_url: Some(format!("{}/backend-api", mock_server.uri())),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
write_models_cache(codex_home.path())?;
|
||||
mock_accounts_check_role(&mock_server, "org-embedded", "standard-user").await;
|
||||
|
||||
let access_token = encode_id_token(
|
||||
&ChatGptIdTokenClaims::new()
|
||||
@@ -313,20 +262,6 @@ async fn set_auth_token_updates_account_and_notifies() -> Result<()> {
|
||||
};
|
||||
assert_eq!(payload.auth_mode, Some(AuthMode::ChatgptAuthTokens));
|
||||
assert_eq!(payload.plan_type, Some(AccountPlanType::Pro));
|
||||
assert_eq!(payload.workspace_role, None);
|
||||
assert_eq!(payload.is_workspace_owner, None);
|
||||
|
||||
let note = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("account/updated"),
|
||||
)
|
||||
.await??;
|
||||
let parsed: ServerNotification = note.try_into()?;
|
||||
let ServerNotification::AccountUpdated(payload) = parsed else {
|
||||
bail!("unexpected notification: {parsed:?}");
|
||||
};
|
||||
assert_eq!(payload.workspace_role, Some(WorkspaceRole::StandardUser));
|
||||
assert_eq!(payload.is_workspace_owner, Some(false));
|
||||
|
||||
let get_id = mcp
|
||||
.send_get_account_request(GetAccountParams {
|
||||
@@ -346,8 +281,6 @@ async fn set_auth_token_updates_account_and_notifies() -> Result<()> {
|
||||
email: "embedded@example.com".to_string(),
|
||||
plan_type: AccountPlanType::Pro,
|
||||
}),
|
||||
workspace_role: None,
|
||||
is_workspace_owner: None,
|
||||
requires_openai_auth: true,
|
||||
}
|
||||
);
|
||||
@@ -415,8 +348,6 @@ async fn account_read_refresh_token_is_noop_in_external_mode() -> Result<()> {
|
||||
email: "embedded@example.com".to_string(),
|
||||
plan_type: AccountPlanType::Pro,
|
||||
}),
|
||||
workspace_role: None,
|
||||
is_workspace_owner: None,
|
||||
requires_openai_auth: true,
|
||||
}
|
||||
);
|
||||
@@ -1574,8 +1505,6 @@ async fn get_account_with_api_key() -> Result<()> {
|
||||
|
||||
let expected = GetAccountResponse {
|
||||
account: Some(Account::ApiKey {}),
|
||||
workspace_role: None,
|
||||
is_workspace_owner: None,
|
||||
requires_openai_auth: true,
|
||||
};
|
||||
assert_eq!(received, expected);
|
||||
@@ -1610,8 +1539,6 @@ async fn get_account_when_auth_not_required() -> Result<()> {
|
||||
|
||||
let expected = GetAccountResponse {
|
||||
account: None,
|
||||
workspace_role: None,
|
||||
is_workspace_owner: None,
|
||||
requires_openai_auth: false,
|
||||
};
|
||||
assert_eq!(received, expected);
|
||||
@@ -1632,8 +1559,7 @@ async fn get_account_with_chatgpt() -> Result<()> {
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("access-chatgpt")
|
||||
.email("user@example.com")
|
||||
.plan_type("pro")
|
||||
.is_org_owner(/*is_org_owner*/ true),
|
||||
.plan_type("pro"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
@@ -1657,190 +1583,12 @@ async fn get_account_with_chatgpt() -> Result<()> {
|
||||
email: "user@example.com".to_string(),
|
||||
plan_type: AccountPlanType::Pro,
|
||||
}),
|
||||
workspace_role: None,
|
||||
is_workspace_owner: Some(true),
|
||||
requires_openai_auth: true,
|
||||
};
|
||||
assert_eq!(received, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_account_with_chatgpt_emits_workspace_role_from_accounts_check() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mock_server = MockServer::start().await;
|
||||
create_config_toml(
|
||||
codex_home.path(),
|
||||
CreateConfigTomlParams {
|
||||
requires_openai_auth: Some(true),
|
||||
chatgpt_base_url: Some(format!("{}/backend-api", mock_server.uri())),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
mock_accounts_check_role(&mock_server, "org-embedded", "account-owner").await;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("access-chatgpt")
|
||||
.account_id("org-embedded")
|
||||
.email("user@example.com")
|
||||
.plan_type("pro")
|
||||
.chatgpt_account_id("org-embedded"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let params = GetAccountParams {
|
||||
refresh_token: false,
|
||||
};
|
||||
let request_id = mcp.send_get_account_request(params).await?;
|
||||
|
||||
let resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let received: GetAccountResponse = to_response(resp)?;
|
||||
|
||||
let expected = GetAccountResponse {
|
||||
account: Some(Account::Chatgpt {
|
||||
email: "user@example.com".to_string(),
|
||||
plan_type: AccountPlanType::Pro,
|
||||
}),
|
||||
workspace_role: None,
|
||||
is_workspace_owner: None,
|
||||
requires_openai_auth: true,
|
||||
};
|
||||
assert_eq!(received, expected);
|
||||
|
||||
let note = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("account/updated"),
|
||||
)
|
||||
.await??;
|
||||
let parsed: ServerNotification = note.try_into()?;
|
||||
let ServerNotification::AccountUpdated(payload) = parsed else {
|
||||
bail!("unexpected notification: {parsed:?}");
|
||||
};
|
||||
assert_eq!(payload.workspace_role, Some(WorkspaceRole::AccountOwner));
|
||||
assert_eq!(payload.is_workspace_owner, Some(true));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_account_with_chatgpt_does_not_guess_workspace_role_from_other_accounts() -> Result<()>
|
||||
{
|
||||
let codex_home = TempDir::new()?;
|
||||
let mock_server = MockServer::start().await;
|
||||
create_config_toml(
|
||||
codex_home.path(),
|
||||
CreateConfigTomlParams {
|
||||
requires_openai_auth: Some(true),
|
||||
chatgpt_base_url: Some(format!("{}/backend-api", mock_server.uri())),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
mock_accounts_check_role(&mock_server, "org-other", "account-owner").await;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("access-chatgpt")
|
||||
.account_id("org-current")
|
||||
.email("user@example.com")
|
||||
.plan_type("pro")
|
||||
.chatgpt_account_id("org-current"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let params = GetAccountParams {
|
||||
refresh_token: false,
|
||||
};
|
||||
let request_id = mcp.send_get_account_request(params).await?;
|
||||
|
||||
let resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let received: GetAccountResponse = to_response(resp)?;
|
||||
|
||||
let expected = GetAccountResponse {
|
||||
account: Some(Account::Chatgpt {
|
||||
email: "user@example.com".to_string(),
|
||||
plan_type: AccountPlanType::Pro,
|
||||
}),
|
||||
workspace_role: None,
|
||||
is_workspace_owner: None,
|
||||
requires_openai_auth: true,
|
||||
};
|
||||
assert_eq!(received, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_account_with_chatgpt_does_not_wait_for_accounts_check() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mock_server = MockServer::start().await;
|
||||
create_config_toml(
|
||||
codex_home.path(),
|
||||
CreateConfigTomlParams {
|
||||
requires_openai_auth: Some(true),
|
||||
chatgpt_base_url: Some(format!("{}/backend-api", mock_server.uri())),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
mock_slow_accounts_check_role(
|
||||
&mock_server,
|
||||
"org-embedded",
|
||||
"standard-user",
|
||||
Duration::from_secs(2),
|
||||
)
|
||||
.await;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("access-chatgpt")
|
||||
.account_id("org-embedded")
|
||||
.email("user@example.com")
|
||||
.plan_type("pro")
|
||||
.chatgpt_account_id("org-embedded")
|
||||
.is_org_owner(/*is_org_owner*/ true),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_get_account_request(GetAccountParams {
|
||||
refresh_token: false,
|
||||
})
|
||||
.await?;
|
||||
let resp: JSONRPCResponse = timeout(
|
||||
Duration::from_millis(500),
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let received: GetAccountResponse = to_response(resp)?;
|
||||
|
||||
assert_eq!(
|
||||
received,
|
||||
GetAccountResponse {
|
||||
account: Some(Account::Chatgpt {
|
||||
email: "user@example.com".to_string(),
|
||||
plan_type: AccountPlanType::Pro,
|
||||
}),
|
||||
workspace_role: None,
|
||||
is_workspace_owner: Some(true),
|
||||
requires_openai_auth: true,
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_account_with_chatgpt_missing_plan_claim_returns_unknown() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -1877,8 +1625,6 @@ async fn get_account_with_chatgpt_missing_plan_claim_returns_unknown() -> Result
|
||||
email: "user@example.com".to_string(),
|
||||
plan_type: AccountPlanType::Unknown,
|
||||
}),
|
||||
workspace_role: None,
|
||||
is_workspace_owner: None,
|
||||
requires_openai_auth: true,
|
||||
};
|
||||
assert_eq!(received, expected);
|
||||
|
||||
@@ -569,6 +569,55 @@ model = "gpt-old"
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn config_read_after_pipelined_write_sees_written_value() -> Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let codex_home = temp_dir.path().canonicalize()?;
|
||||
write_config(
|
||||
&temp_dir,
|
||||
r#"
|
||||
model = "gpt-old"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(&codex_home).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let write_id = mcp
|
||||
.send_config_value_write_request(ConfigValueWriteParams {
|
||||
file_path: None,
|
||||
key_path: "model".to_string(),
|
||||
value: json!("gpt-new"),
|
||||
merge_strategy: MergeStrategy::Replace,
|
||||
expected_version: None,
|
||||
})
|
||||
.await?;
|
||||
let read_id = mcp
|
||||
.send_config_read_request(ConfigReadParams {
|
||||
include_layers: false,
|
||||
cwd: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let write_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(write_id)),
|
||||
)
|
||||
.await??;
|
||||
let write: ConfigWriteResponse = to_response(write_resp)?;
|
||||
assert_eq!(write.status, WriteStatus::Ok);
|
||||
|
||||
let read_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(read_id)),
|
||||
)
|
||||
.await??;
|
||||
let read: ConfigReadResponse = to_response(read_resp)?;
|
||||
assert_eq!(read.config.model.as_deref(), Some("gpt-new"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn config_value_write_rejects_version_conflict() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -32,7 +32,6 @@ mod request_user_input;
|
||||
mod review;
|
||||
mod safety_check_downgrade;
|
||||
mod skills_list;
|
||||
mod thread_add_credits_nudge_email;
|
||||
mod thread_archive;
|
||||
mod thread_fork;
|
||||
mod thread_list;
|
||||
|
||||
@@ -133,10 +133,7 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"spend_control": {
|
||||
"reached": true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
Mock::given(method("GET"))
|
||||
@@ -175,7 +172,6 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> {
|
||||
resets_at: Some(secondary_reset_timestamp),
|
||||
}),
|
||||
credits: None,
|
||||
spend_control: Some(codex_app_server_protocol::SpendControlSnapshot { reached: true }),
|
||||
plan_type: Some(AccountPlanType::Pro),
|
||||
},
|
||||
rate_limits_by_limit_id: Some(
|
||||
@@ -196,9 +192,6 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> {
|
||||
resets_at: Some(secondary_reset_timestamp),
|
||||
}),
|
||||
credits: None,
|
||||
spend_control: Some(codex_app_server_protocol::SpendControlSnapshot {
|
||||
reached: true,
|
||||
}),
|
||||
plan_type: Some(AccountPlanType::Pro),
|
||||
},
|
||||
),
|
||||
@@ -214,7 +207,6 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> {
|
||||
}),
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: Some(AccountPlanType::Pro),
|
||||
},
|
||||
),
|
||||
|
||||
@@ -241,6 +241,7 @@ async fn skills_changed_notification_is_emitted_after_skill_change() -> Result<(
|
||||
developer_instructions: None,
|
||||
personality: None,
|
||||
ephemeral: None,
|
||||
session_start_source: None,
|
||||
dynamic_tools: None,
|
||||
mock_experimental_field: None,
|
||||
experimental_raw_events: false,
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use app_test_support::McpProcess;
|
||||
use app_test_support::create_mock_responses_server_sequence;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server_protocol::AddCreditsNudgeEmailNotification;
|
||||
use codex_app_server_protocol::AddCreditsNudgeEmailResult;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ThreadAddCreditsNudgeEmailParams;
|
||||
use codex_app_server_protocol::ThreadAddCreditsNudgeEmailResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_add_credits_nudge_email_submits_core_op_and_emits_completion() -> Result<()> {
|
||||
let tmp = TempDir::new()?;
|
||||
let codex_home = tmp.path().join("codex_home");
|
||||
std::fs::create_dir(&codex_home)?;
|
||||
|
||||
let server = create_mock_responses_server_sequence(vec![]).await;
|
||||
create_config_toml(codex_home.as_path(), &server.uri())?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.as_path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let start_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams::default())
|
||||
.await?;
|
||||
let start_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(start_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(start_resp)?;
|
||||
|
||||
let nudge_id = mcp
|
||||
.send_thread_add_credits_nudge_email_request(ThreadAddCreditsNudgeEmailParams {
|
||||
thread_id: thread.id.clone(),
|
||||
})
|
||||
.await?;
|
||||
let nudge_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(nudge_id)),
|
||||
)
|
||||
.await??;
|
||||
let _: ThreadAddCreditsNudgeEmailResponse =
|
||||
to_response::<ThreadAddCreditsNudgeEmailResponse>(nudge_resp)?;
|
||||
|
||||
let notification: AddCreditsNudgeEmailNotification = serde_json::from_value(
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("account/addCreditsNudgeEmail/completed"),
|
||||
)
|
||||
.await??
|
||||
.params
|
||||
.expect("account/addCreditsNudgeEmail/completed params"),
|
||||
)?;
|
||||
|
||||
assert_eq!(notification.thread_id, thread.id);
|
||||
assert_eq!(
|
||||
notification.result,
|
||||
AddCreditsNudgeEmailResult::Failed {
|
||||
message: "codex account authentication required to notify workspace owner".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()> {
|
||||
std::fs::write(
|
||||
codex_home.join("config.toml"),
|
||||
format!(
|
||||
r#"
|
||||
model = "mock-model"
|
||||
approval_policy = "never"
|
||||
sandbox_mode = "read-only"
|
||||
|
||||
model_provider = "mock_provider"
|
||||
|
||||
[model_providers.mock_provider]
|
||||
name = "Mock provider for test"
|
||||
base_url = "{server_uri}/v1"
|
||||
wire_api = "responses"
|
||||
request_max_retries = 0
|
||||
stream_max_retries = 0
|
||||
"#
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
use crate::types::AccountsCheckV4Response;
|
||||
use crate::types::CodeTaskDetailsResponse;
|
||||
use crate::types::ConfigFileResponse;
|
||||
use crate::types::PaginatedListTaskListItem;
|
||||
use crate::types::RateLimitStatusPayload;
|
||||
use crate::types::TurnAttemptsSiblingTurnsResponse;
|
||||
use crate::types::WorkspaceRole;
|
||||
use anyhow::Result;
|
||||
use codex_client::build_reqwest_client_with_custom_ca;
|
||||
use codex_login::CodexAuth;
|
||||
@@ -13,7 +11,6 @@ use codex_protocol::account::PlanType as AccountPlanType;
|
||||
use codex_protocol::protocol::CreditsSnapshot;
|
||||
use codex_protocol::protocol::RateLimitSnapshot;
|
||||
use codex_protocol::protocol::RateLimitWindow;
|
||||
use codex_protocol::protocol::SpendControlSnapshot;
|
||||
use reqwest::StatusCode;
|
||||
use reqwest::header::AUTHORIZATION;
|
||||
use reqwest::header::CONTENT_TYPE;
|
||||
@@ -23,10 +20,6 @@ use reqwest::header::HeaderValue;
|
||||
use reqwest::header::USER_AGENT;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::fmt;
|
||||
use std::time::Duration;
|
||||
|
||||
const BACKEND_REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const ACCOUNTS_CHECK_V4_VERSION: &str = "v4-2023-04-27";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RequestError {
|
||||
@@ -266,55 +259,12 @@ impl Client {
|
||||
PathStyle::CodexApi => format!("{}/api/codex/usage", self.base_url),
|
||||
PathStyle::ChatGptApi => format!("{}/wham/usage", self.base_url),
|
||||
};
|
||||
let req = self
|
||||
.http
|
||||
.get(&url)
|
||||
.headers(self.headers())
|
||||
.timeout(BACKEND_REQUEST_TIMEOUT);
|
||||
let req = self.http.get(&url).headers(self.headers());
|
||||
let (body, ct) = self.exec_request(req, "GET", &url).await?;
|
||||
let payload: RateLimitStatusPayload = self.decode_json(&url, &ct, &body)?;
|
||||
Ok(Self::rate_limit_snapshots_from_payload(payload))
|
||||
}
|
||||
|
||||
pub async fn send_add_credits_nudge_email(&self) -> std::result::Result<(), RequestError> {
|
||||
let url = match self.path_style {
|
||||
PathStyle::CodexApi => {
|
||||
format!(
|
||||
"{}/api/codex/accounts/send_add_credits_nudge_email",
|
||||
self.base_url
|
||||
)
|
||||
}
|
||||
PathStyle::ChatGptApi => {
|
||||
format!("{}/accounts/send_add_credits_nudge_email", self.base_url)
|
||||
}
|
||||
};
|
||||
let req = self
|
||||
.http
|
||||
.post(&url)
|
||||
.headers(self.headers())
|
||||
.timeout(BACKEND_REQUEST_TIMEOUT);
|
||||
let _ = self.exec_request_detailed(req, "POST", &url).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_current_workspace_role(&self) -> Result<Option<WorkspaceRole>> {
|
||||
if self.path_style != PathStyle::ChatGptApi {
|
||||
return Ok(None);
|
||||
}
|
||||
let url = format!(
|
||||
"{}/accounts/check/{ACCOUNTS_CHECK_V4_VERSION}",
|
||||
self.base_url
|
||||
);
|
||||
let req = self
|
||||
.http
|
||||
.get(&url)
|
||||
.headers(self.headers())
|
||||
.timeout(BACKEND_REQUEST_TIMEOUT);
|
||||
let (body, ct) = self.exec_request(req, "GET", &url).await?;
|
||||
let payload: AccountsCheckV4Response = self.decode_json(&url, &ct, &body)?;
|
||||
Ok(payload.current_workspace_role(self.chatgpt_account_id.as_deref()))
|
||||
}
|
||||
|
||||
pub async fn list_tasks(
|
||||
&self,
|
||||
limit: Option<i32>,
|
||||
@@ -447,23 +397,20 @@ impl Client {
|
||||
payload: RateLimitStatusPayload,
|
||||
) -> Vec<RateLimitSnapshot> {
|
||||
let plan_type = Some(Self::map_plan_type(payload.plan_type));
|
||||
let spend_control = payload.spend_control.map(|details| *details);
|
||||
let mut snapshots = vec![Self::make_rate_limit_snapshot(
|
||||
Some("codex".to_string()),
|
||||
/*limit_name*/ None,
|
||||
payload.rate_limit.map(|details| *details),
|
||||
payload.credits.map(|details| *details),
|
||||
spend_control,
|
||||
payload.rate_limit.flatten().map(|details| *details),
|
||||
payload.credits.flatten().map(|details| *details),
|
||||
plan_type,
|
||||
)];
|
||||
if let Some(additional) = payload.additional_rate_limits {
|
||||
if let Some(additional) = payload.additional_rate_limits.flatten() {
|
||||
snapshots.extend(additional.into_iter().map(|details| {
|
||||
Self::make_rate_limit_snapshot(
|
||||
Some(details.metered_feature),
|
||||
Some(details.limit_name),
|
||||
details.rate_limit.flatten().map(|rate_limit| *rate_limit),
|
||||
/*credits*/ None,
|
||||
/*spend_control*/ None,
|
||||
plan_type,
|
||||
)
|
||||
}));
|
||||
@@ -476,7 +423,6 @@ impl Client {
|
||||
limit_name: Option<String>,
|
||||
rate_limit: Option<crate::types::RateLimitStatusDetails>,
|
||||
credits: Option<crate::types::CreditStatusDetails>,
|
||||
spend_control: Option<crate::types::SpendControlStatusDetails>,
|
||||
plan_type: Option<AccountPlanType>,
|
||||
) -> RateLimitSnapshot {
|
||||
let (primary, secondary) = match rate_limit {
|
||||
@@ -492,7 +438,6 @@ impl Client {
|
||||
primary,
|
||||
secondary,
|
||||
credits: Self::map_credits(credits),
|
||||
spend_control: Self::map_spend_control(spend_control),
|
||||
plan_type,
|
||||
}
|
||||
}
|
||||
@@ -522,15 +467,6 @@ impl Client {
|
||||
})
|
||||
}
|
||||
|
||||
fn map_spend_control(
|
||||
spend_control: Option<crate::types::SpendControlStatusDetails>,
|
||||
) -> Option<SpendControlSnapshot> {
|
||||
let details = spend_control?;
|
||||
Some(SpendControlSnapshot {
|
||||
reached: details.reached,
|
||||
})
|
||||
}
|
||||
|
||||
fn map_plan_type(plan_type: crate::types::PlanType) -> AccountPlanType {
|
||||
match plan_type {
|
||||
crate::types::PlanType::Free => AccountPlanType::Free,
|
||||
@@ -567,6 +503,7 @@ impl Client {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_backend_openapi_models::models::AdditionalRateLimitDetails;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
@@ -585,7 +522,7 @@ mod tests {
|
||||
fn usage_payload_maps_primary_and_additional_rate_limits() {
|
||||
let payload = RateLimitStatusPayload {
|
||||
plan_type: crate::types::PlanType::Pro,
|
||||
rate_limit: Some(Box::new(crate::types::RateLimitStatusDetails {
|
||||
rate_limit: Some(Some(Box::new(crate::types::RateLimitStatusDetails {
|
||||
primary_window: Some(Some(Box::new(crate::types::RateLimitWindowSnapshot {
|
||||
used_percent: 42,
|
||||
limit_window_seconds: 300,
|
||||
@@ -599,8 +536,8 @@ mod tests {
|
||||
reset_at: 456,
|
||||
}))),
|
||||
..Default::default()
|
||||
})),
|
||||
additional_rate_limits: Some(vec![crate::types::AdditionalRateLimitDetails {
|
||||
}))),
|
||||
additional_rate_limits: Some(Some(vec![AdditionalRateLimitDetails {
|
||||
limit_name: "codex_other".to_string(),
|
||||
metered_feature: "codex_other".to_string(),
|
||||
rate_limit: Some(Some(Box::new(crate::types::RateLimitStatusDetails {
|
||||
@@ -613,16 +550,13 @@ mod tests {
|
||||
secondary_window: None,
|
||||
..Default::default()
|
||||
}))),
|
||||
}]),
|
||||
credits: Some(Box::new(crate::types::CreditStatusDetails {
|
||||
}])),
|
||||
credits: Some(Some(Box::new(crate::types::CreditStatusDetails {
|
||||
has_credits: true,
|
||||
unlimited: false,
|
||||
balance: Some(Some("9.99".to_string())),
|
||||
..Default::default()
|
||||
})),
|
||||
spend_control: Some(Box::new(crate::types::SpendControlStatusDetails {
|
||||
reached: true,
|
||||
})),
|
||||
}))),
|
||||
};
|
||||
|
||||
let snapshots = Client::rate_limit_snapshots_from_payload(payload);
|
||||
@@ -646,10 +580,6 @@ mod tests {
|
||||
balance: Some("9.99".to_string()),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
snapshots[0].spend_control,
|
||||
Some(SpendControlSnapshot { reached: true })
|
||||
);
|
||||
assert_eq!(snapshots[0].plan_type, Some(AccountPlanType::Pro));
|
||||
|
||||
assert_eq!(snapshots[1].limit_id.as_deref(), Some("codex_other"));
|
||||
@@ -659,7 +589,6 @@ mod tests {
|
||||
Some(70.0)
|
||||
);
|
||||
assert_eq!(snapshots[1].credits, None);
|
||||
assert_eq!(snapshots[1].spend_control, None);
|
||||
assert_eq!(snapshots[1].plan_type, Some(AccountPlanType::Pro));
|
||||
}
|
||||
|
||||
@@ -668,13 +597,12 @@ mod tests {
|
||||
let payload = RateLimitStatusPayload {
|
||||
plan_type: crate::types::PlanType::Plus,
|
||||
rate_limit: None,
|
||||
additional_rate_limits: Some(vec![crate::types::AdditionalRateLimitDetails {
|
||||
additional_rate_limits: Some(Some(vec![AdditionalRateLimitDetails {
|
||||
limit_name: "codex_other".to_string(),
|
||||
metered_feature: "codex_other".to_string(),
|
||||
rate_limit: None,
|
||||
}]),
|
||||
}])),
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
};
|
||||
|
||||
let snapshots = Client::rate_limit_snapshots_from_payload(payload);
|
||||
@@ -682,7 +610,6 @@ mod tests {
|
||||
assert_eq!(snapshots[0].limit_id.as_deref(), Some("codex"));
|
||||
assert_eq!(snapshots[0].limit_name, None);
|
||||
assert_eq!(snapshots[0].primary, None);
|
||||
assert_eq!(snapshots[0].spend_control, None);
|
||||
assert_eq!(snapshots[1].limit_id.as_deref(), Some("codex_other"));
|
||||
assert_eq!(snapshots[1].limit_name.as_deref(), Some("codex_other"));
|
||||
}
|
||||
@@ -700,7 +627,6 @@ mod tests {
|
||||
}),
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: Some(AccountPlanType::Pro),
|
||||
},
|
||||
RateLimitSnapshot {
|
||||
@@ -713,7 +639,6 @@ mod tests {
|
||||
}),
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: Some(AccountPlanType::Pro),
|
||||
},
|
||||
];
|
||||
@@ -725,46 +650,4 @@ mod tests {
|
||||
.unwrap_or_else(|| snapshots[0].clone());
|
||||
assert_eq!(preferred.limit_id.as_deref(), Some("codex"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_credits_nudge_email_uses_expected_paths() {
|
||||
let codex_api = Client::new("https://example.com").expect("codex api client");
|
||||
assert_eq!(
|
||||
match codex_api.path_style {
|
||||
PathStyle::CodexApi => format!(
|
||||
"{}/api/codex/accounts/send_add_credits_nudge_email",
|
||||
codex_api.base_url
|
||||
),
|
||||
PathStyle::ChatGptApi => unreachable!("plain host should use codex api paths"),
|
||||
},
|
||||
"https://example.com/api/codex/accounts/send_add_credits_nudge_email"
|
||||
);
|
||||
|
||||
let chatgpt_api = Client::new("https://chatgpt.com").expect("chatgpt backend api client");
|
||||
assert_eq!(
|
||||
match chatgpt_api.path_style {
|
||||
PathStyle::CodexApi => unreachable!("chatgpt host should use backend-api paths"),
|
||||
PathStyle::ChatGptApi => format!(
|
||||
"{}/accounts/send_add_credits_nudge_email",
|
||||
chatgpt_api.base_url
|
||||
),
|
||||
},
|
||||
"https://chatgpt.com/backend-api/accounts/send_add_credits_nudge_email"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_workspace_role_uses_expected_path() {
|
||||
let chatgpt_api = Client::new("https://chatgpt.com").expect("chatgpt backend api client");
|
||||
assert_eq!(
|
||||
match chatgpt_api.path_style {
|
||||
PathStyle::CodexApi => unreachable!("chatgpt host should use backend-api paths"),
|
||||
PathStyle::ChatGptApi => format!(
|
||||
"{}/accounts/check/{ACCOUNTS_CHECK_V4_VERSION}",
|
||||
chatgpt_api.base_url
|
||||
),
|
||||
},
|
||||
"https://chatgpt.com/backend-api/accounts/check/v4-2023-04-27"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,9 @@ pub(crate) mod types;
|
||||
|
||||
pub use client::Client;
|
||||
pub use client::RequestError;
|
||||
pub use types::AccountsCheckV4Response;
|
||||
pub use types::CodeTaskDetailsResponse;
|
||||
pub use types::CodeTaskDetailsResponseExt;
|
||||
pub use types::ConfigFileResponse;
|
||||
pub use types::PaginatedListTaskListItem;
|
||||
pub use types::TaskListItem;
|
||||
pub use types::TurnAttemptsSiblingTurnsResponse;
|
||||
pub use types::WorkspaceRole;
|
||||
|
||||
@@ -1,115 +1,17 @@
|
||||
pub use codex_backend_openapi_models::models::AdditionalRateLimitDetails;
|
||||
pub use codex_backend_openapi_models::models::ConfigFileResponse;
|
||||
pub use codex_backend_openapi_models::models::CreditStatusDetails;
|
||||
pub use codex_backend_openapi_models::models::PaginatedListTaskListItem;
|
||||
pub use codex_backend_openapi_models::models::PlanType;
|
||||
pub use codex_backend_openapi_models::models::RateLimitStatusDetails;
|
||||
pub use codex_backend_openapi_models::models::RateLimitStatusPayload;
|
||||
pub use codex_backend_openapi_models::models::RateLimitWindowSnapshot;
|
||||
pub use codex_backend_openapi_models::models::TaskListItem;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde::de::Deserializer;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum WorkspaceRole {
|
||||
AccountOwner,
|
||||
AccountAdmin,
|
||||
StandardUser,
|
||||
}
|
||||
|
||||
impl WorkspaceRole {
|
||||
pub fn from_api_str(value: &str) -> Option<Self> {
|
||||
match value {
|
||||
"account-owner" | "account_owner" => Some(Self::AccountOwner),
|
||||
"account-admin" | "account_admin" => Some(Self::AccountAdmin),
|
||||
"standard-user" | "standard_user" | "member" => Some(Self::StandardUser),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
pub struct AccountsCheckV4Response {
|
||||
#[serde(default)]
|
||||
pub accounts: HashMap<String, AccountsCheckV4AccountItem>,
|
||||
#[serde(default)]
|
||||
pub account_ordering: Vec<String>,
|
||||
}
|
||||
|
||||
impl AccountsCheckV4Response {
|
||||
pub fn current_workspace_role(
|
||||
&self,
|
||||
current_account_id: Option<&str>,
|
||||
) -> Option<WorkspaceRole> {
|
||||
let account = if let Some(account_id) = current_account_id {
|
||||
self.accounts.get(account_id)?
|
||||
} else {
|
||||
self.account_ordering
|
||||
.iter()
|
||||
.find_map(|account_id| self.accounts.get(account_id))
|
||||
.or_else(|| {
|
||||
if self.accounts.len() == 1 {
|
||||
self.accounts.values().next()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})?
|
||||
};
|
||||
account
|
||||
.account
|
||||
.as_ref()
|
||||
.and_then(|account| account.account_user_role.as_deref())
|
||||
.and_then(WorkspaceRole::from_api_str)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
pub struct AccountsCheckV4AccountItem {
|
||||
#[serde(default)]
|
||||
pub account: Option<AccountsCheckV4Account>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
pub struct AccountsCheckV4Account {
|
||||
#[serde(default, rename = "account_user_role")]
|
||||
pub account_user_role: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RateLimitStatusPayload {
|
||||
#[serde(rename = "plan_type")]
|
||||
pub plan_type: PlanType,
|
||||
#[serde(
|
||||
rename = "rate_limit",
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub rate_limit: Option<Box<RateLimitStatusDetails>>,
|
||||
#[serde(rename = "credits", default, skip_serializing_if = "Option::is_none")]
|
||||
pub credits: Option<Box<CreditStatusDetails>>,
|
||||
#[serde(
|
||||
rename = "additional_rate_limits",
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub additional_rate_limits: Option<Vec<AdditionalRateLimitDetails>>,
|
||||
#[serde(
|
||||
rename = "spend_control",
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub spend_control: Option<Box<SpendControlStatusDetails>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SpendControlStatusDetails {
|
||||
#[serde(rename = "reached")]
|
||||
pub reached: bool,
|
||||
}
|
||||
|
||||
/// Hand-rolled models for the Cloud Tasks task-details response.
|
||||
/// The generated OpenAPI models are pretty bad. This is a half-step
|
||||
/// towards hand-rolling them.
|
||||
@@ -318,83 +220,6 @@ impl Turn {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod accounts_check_tests {
|
||||
use super::AccountsCheckV4Account;
|
||||
use super::AccountsCheckV4AccountItem;
|
||||
use super::AccountsCheckV4Response;
|
||||
use super::WorkspaceRole;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn current_workspace_role_prefers_current_account_id() {
|
||||
let response = AccountsCheckV4Response {
|
||||
accounts: HashMap::from([
|
||||
(
|
||||
"workspace-a".to_string(),
|
||||
AccountsCheckV4AccountItem {
|
||||
account: Some(AccountsCheckV4Account {
|
||||
account_user_role: Some("standard-user".to_string()),
|
||||
}),
|
||||
},
|
||||
),
|
||||
(
|
||||
"workspace-b".to_string(),
|
||||
AccountsCheckV4AccountItem {
|
||||
account: Some(AccountsCheckV4Account {
|
||||
account_user_role: Some("account-owner".to_string()),
|
||||
}),
|
||||
},
|
||||
),
|
||||
]),
|
||||
account_ordering: vec!["workspace-a".to_string(), "workspace-b".to_string()],
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
response.current_workspace_role(Some("workspace-b")),
|
||||
Some(WorkspaceRole::AccountOwner)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_workspace_role_falls_back_to_account_ordering() {
|
||||
let response = AccountsCheckV4Response {
|
||||
accounts: HashMap::from([(
|
||||
"workspace-a".to_string(),
|
||||
AccountsCheckV4AccountItem {
|
||||
account: Some(AccountsCheckV4Account {
|
||||
account_user_role: Some("account_admin".to_string()),
|
||||
}),
|
||||
},
|
||||
)]),
|
||||
account_ordering: vec!["workspace-a".to_string()],
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
response.current_workspace_role(/*current_account_id*/ None),
|
||||
Some(WorkspaceRole::AccountAdmin)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_workspace_role_does_not_fall_back_when_current_account_missing() {
|
||||
let response = AccountsCheckV4Response {
|
||||
accounts: HashMap::from([(
|
||||
"workspace-a".to_string(),
|
||||
AccountsCheckV4AccountItem {
|
||||
account: Some(AccountsCheckV4Account {
|
||||
account_user_role: Some("account-owner".to_string()),
|
||||
}),
|
||||
},
|
||||
)]),
|
||||
account_ordering: vec!["workspace-a".to_string()],
|
||||
};
|
||||
|
||||
assert_eq!(response.current_workspace_role(Some("workspace-b")), None);
|
||||
}
|
||||
}
|
||||
|
||||
impl WorklogMessage {
|
||||
fn is_assistant(&self) -> bool {
|
||||
self.author
|
||||
|
||||
@@ -92,7 +92,6 @@ pub fn parse_rate_limit_for_limit(
|
||||
primary,
|
||||
secondary,
|
||||
credits,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
})
|
||||
}
|
||||
@@ -156,7 +155,6 @@ pub fn parse_rate_limit_event(payload: &str) -> Option<RateLimitSnapshot> {
|
||||
primary,
|
||||
secondary,
|
||||
credits,
|
||||
spend_control: None,
|
||||
plan_type: event.plan_type,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ bm25 = { workspace = true }
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
codex-analytics = { workspace = true }
|
||||
codex-account = { workspace = true }
|
||||
codex-api = { workspace = true }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-apply-patch = { workspace = true }
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
use crate::config::Config;
|
||||
use codex_account::AddCreditsNudgeEmailStatus;
|
||||
use codex_account::SendAddCreditsNudgeEmailError;
|
||||
use codex_account::WorkspaceOwnership;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
|
||||
pub async fn send_add_credits_nudge_email(
|
||||
config: &Config,
|
||||
auth_manager: &AuthManager,
|
||||
) -> Result<AddCreditsNudgeEmailStatus, SendAddCreditsNudgeEmailError> {
|
||||
codex_account::send_add_credits_nudge_email(config.chatgpt_base_url.clone(), auth_manager).await
|
||||
}
|
||||
|
||||
pub async fn resolve_workspace_role_and_owner_for_auth(
|
||||
chatgpt_base_url: &str,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> WorkspaceOwnership {
|
||||
codex_account::resolve_workspace_role_and_owner_for_auth(chatgpt_base_url, auth).await
|
||||
}
|
||||
@@ -599,7 +599,7 @@ impl Codex {
|
||||
let thread_id = match &conversation_history {
|
||||
InitialHistory::Resumed(resumed) => Some(resumed.conversation_id),
|
||||
InitialHistory::Forked(_) => conversation_history.forked_from_id(),
|
||||
InitialHistory::New => None,
|
||||
InitialHistory::New | InitialHistory::Cleared => None,
|
||||
};
|
||||
match thread_id {
|
||||
Some(thread_id) => {
|
||||
@@ -1619,7 +1619,7 @@ impl Session {
|
||||
let forked_from_id = initial_history.forked_from_id();
|
||||
|
||||
let (conversation_id, rollout_params) = match &initial_history {
|
||||
InitialHistory::New | InitialHistory::Forked(_) => {
|
||||
InitialHistory::New | InitialHistory::Cleared | InitialHistory::Forked(_) => {
|
||||
let conversation_id = ThreadId::default();
|
||||
(
|
||||
conversation_id,
|
||||
@@ -1660,14 +1660,14 @@ impl Session {
|
||||
.count(),
|
||||
)
|
||||
.unwrap_or(u64::MAX),
|
||||
InitialHistory::New | InitialHistory::Forked(_) => 0,
|
||||
InitialHistory::New | InitialHistory::Cleared | InitialHistory::Forked(_) => 0,
|
||||
};
|
||||
let state_builder = match &initial_history {
|
||||
InitialHistory::Resumed(resumed) => metadata::builder_from_items(
|
||||
resumed.history.as_slice(),
|
||||
resumed.rollout_path.as_path(),
|
||||
),
|
||||
InitialHistory::New | InitialHistory::Forked(_) => None,
|
||||
InitialHistory::New | InitialHistory::Cleared | InitialHistory::Forked(_) => None,
|
||||
};
|
||||
|
||||
// Kick off independent async setup tasks in parallel to reduce startup latency.
|
||||
@@ -2026,7 +2026,7 @@ impl Session {
|
||||
session_telemetry,
|
||||
models_manager: Arc::clone(&models_manager),
|
||||
tool_approvals: Mutex::new(ApprovalStore::default()),
|
||||
guardian_rejection_rationales: Mutex::new(HashMap::new()),
|
||||
guardian_rejections: Mutex::new(HashMap::new()),
|
||||
skills_manager,
|
||||
plugins_manager: Arc::clone(&plugins_manager),
|
||||
mcp_manager: Arc::clone(&mcp_manager),
|
||||
@@ -2199,6 +2199,7 @@ impl Session {
|
||||
InitialHistory::New | InitialHistory::Forked(_) => {
|
||||
codex_hooks::SessionStartSource::Startup
|
||||
}
|
||||
InitialHistory::Cleared => codex_hooks::SessionStartSource::Clear,
|
||||
};
|
||||
|
||||
// record_initial_history can emit events. We record only after the SessionConfiguredEvent is emitted.
|
||||
@@ -2339,7 +2340,7 @@ impl Session {
|
||||
)
|
||||
};
|
||||
match conversation_history {
|
||||
InitialHistory::New => {
|
||||
InitialHistory::New | InitialHistory::Cleared => {
|
||||
// Defer initial context insertion until the first real turn starts so
|
||||
// turn/start overrides can be merged before we write model-visible context.
|
||||
self.set_previous_turn_settings(/*previous_turn_settings*/ None)
|
||||
@@ -4795,10 +4796,6 @@ async fn submission_loop(sess: Arc<Session>, config: Arc<Config>, rx_sub: Receiv
|
||||
handlers::set_thread_name(&sess, sub.id.clone(), name).await;
|
||||
false
|
||||
}
|
||||
Op::SendAddCreditsNudgeEmail => {
|
||||
handlers::send_add_credits_nudge_email(&sess, &config, sub.id.clone()).await;
|
||||
false
|
||||
}
|
||||
Op::RunUserShellCommand { command } => {
|
||||
handlers::run_user_shell_command(&sess, sub.id.clone(), command).await;
|
||||
false
|
||||
@@ -4895,8 +4892,6 @@ mod handlers {
|
||||
use crate::tasks::execute_user_shell_command;
|
||||
use codex_mcp::collect_mcp_snapshot_from_manager;
|
||||
use codex_mcp::compute_auth_statuses;
|
||||
use codex_protocol::protocol::AddCreditsNudgeEmailResponseEvent;
|
||||
use codex_protocol::protocol::AddCreditsNudgeEmailStatus;
|
||||
use codex_protocol::protocol::CodexErrorInfo;
|
||||
use codex_protocol::protocol::ErrorEvent;
|
||||
use codex_protocol::protocol::Event;
|
||||
@@ -4941,48 +4936,6 @@ mod handlers {
|
||||
sess.close_unified_exec_processes().await;
|
||||
}
|
||||
|
||||
pub async fn send_add_credits_nudge_email(
|
||||
sess: &Arc<Session>,
|
||||
config: &Config,
|
||||
sub_id: String,
|
||||
) {
|
||||
match crate::send_add_credits_nudge_email(config, &sess.services.auth_manager).await {
|
||||
Ok(status) => {
|
||||
sess.send_event_raw(Event {
|
||||
id: sub_id,
|
||||
msg: EventMsg::AddCreditsNudgeEmailResponse(
|
||||
AddCreditsNudgeEmailResponseEvent {
|
||||
result: Ok(add_credits_nudge_email_status_to_event(status)),
|
||||
},
|
||||
),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
Err(err) => {
|
||||
sess.send_event_raw(Event {
|
||||
id: sub_id,
|
||||
msg: EventMsg::AddCreditsNudgeEmailResponse(
|
||||
AddCreditsNudgeEmailResponseEvent {
|
||||
result: Err(err.to_string()),
|
||||
},
|
||||
),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_credits_nudge_email_status_to_event(
|
||||
status: crate::AddCreditsNudgeEmailStatus,
|
||||
) -> AddCreditsNudgeEmailStatus {
|
||||
match status {
|
||||
crate::AddCreditsNudgeEmailStatus::Sent => AddCreditsNudgeEmailStatus::Sent,
|
||||
crate::AddCreditsNudgeEmailStatus::CooldownActive => {
|
||||
AddCreditsNudgeEmailStatus::CooldownActive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn realtime_conversation_list_voices(sess: &Session, sub_id: String) {
|
||||
sess.send_event_raw(Event {
|
||||
id: sub_id,
|
||||
@@ -7289,7 +7242,6 @@ fn realtime_text_for_event(msg: &EventMsg) -> Option<String> {
|
||||
| EventMsg::GetHistoryEntryResponse(_)
|
||||
| EventMsg::McpListToolsResponse(_)
|
||||
| EventMsg::ListSkillsResponse(_)
|
||||
| EventMsg::AddCreditsNudgeEmailResponse(_)
|
||||
| EventMsg::RealtimeConversationListVoicesResponse(_)
|
||||
| EventMsg::SkillsUpdateAvailable
|
||||
| EventMsg::PlanUpdate(_)
|
||||
|
||||
@@ -39,6 +39,7 @@ use crate::codex::TurnContext;
|
||||
use crate::codex::emit_subagent_session_started;
|
||||
use crate::config::Config;
|
||||
use crate::guardian::GuardianApprovalRequest;
|
||||
use crate::guardian::new_guardian_review_id;
|
||||
use crate::guardian::review_approval_request_with_cancel;
|
||||
use crate::guardian::routes_approval_to_guardian;
|
||||
use crate::mcp_tool_call::MCP_TOOL_APPROVAL_ACCEPT;
|
||||
@@ -459,6 +460,7 @@ async fn handle_exec_approval(
|
||||
let review_rx = spawn_guardian_review(
|
||||
Arc::clone(parent_session),
|
||||
Arc::clone(parent_ctx),
|
||||
new_guardian_review_id(),
|
||||
GuardianApprovalRequest::Shell {
|
||||
id: call_id.clone(),
|
||||
command,
|
||||
@@ -566,6 +568,7 @@ async fn handle_patch_approval(
|
||||
let review_rx = spawn_guardian_review(
|
||||
Arc::clone(parent_session),
|
||||
Arc::clone(parent_ctx),
|
||||
new_guardian_review_id(),
|
||||
GuardianApprovalRequest::ApplyPatch {
|
||||
id: approval_id.clone(),
|
||||
cwd: parent_ctx.cwd.to_path_buf(),
|
||||
@@ -686,6 +689,7 @@ async fn maybe_auto_review_mcp_request_user_input(
|
||||
let review_rx = spawn_guardian_review(
|
||||
Arc::clone(parent_session),
|
||||
Arc::clone(parent_ctx),
|
||||
new_guardian_review_id(),
|
||||
build_guardian_mcp_tool_review_request(&event.call_id, &invocation, metadata.as_ref()),
|
||||
/*retry_reason*/ None,
|
||||
review_cancel.clone(),
|
||||
@@ -729,6 +733,7 @@ async fn maybe_auto_review_mcp_request_user_input(
|
||||
fn spawn_guardian_review(
|
||||
session: Arc<Session>,
|
||||
turn: Arc<TurnContext>,
|
||||
review_id: String,
|
||||
request: GuardianApprovalRequest,
|
||||
retry_reason: Option<String>,
|
||||
cancel_token: CancellationToken,
|
||||
@@ -745,6 +750,7 @@ fn spawn_guardian_review(
|
||||
let decision = runtime.block_on(review_approval_request_with_cancel(
|
||||
&session,
|
||||
&turn,
|
||||
review_id,
|
||||
request,
|
||||
retry_reason,
|
||||
cancel_token,
|
||||
|
||||
@@ -10,7 +10,6 @@ use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::ExecApprovalRequestEvent;
|
||||
use codex_protocol::protocol::GuardianAssessmentAction;
|
||||
use codex_protocol::protocol::GuardianAssessmentEvent;
|
||||
use codex_protocol::protocol::GuardianAssessmentStatus;
|
||||
use codex_protocol::protocol::GuardianCommandSource;
|
||||
use codex_protocol::protocol::McpInvocation;
|
||||
@@ -311,22 +310,26 @@ async fn handle_exec_approval_uses_call_id_for_guardian_review_and_approval_id_f
|
||||
})
|
||||
.await
|
||||
.expect("timed out waiting for guardian assessment");
|
||||
let expected_action = GuardianAssessmentAction::Command {
|
||||
source: GuardianCommandSource::Shell,
|
||||
command: "rm -rf tmp".to_string(),
|
||||
cwd: "/tmp".into(),
|
||||
};
|
||||
assert!(!assessment_event.id.is_empty());
|
||||
assert_eq!(
|
||||
assessment_event,
|
||||
GuardianAssessmentEvent {
|
||||
id: "command-item-1".to_string(),
|
||||
turn_id: parent_ctx.sub_id.clone(),
|
||||
status: GuardianAssessmentStatus::InProgress,
|
||||
risk_level: None,
|
||||
user_authorization: None,
|
||||
rationale: None,
|
||||
action: GuardianAssessmentAction::Command {
|
||||
source: GuardianCommandSource::Shell,
|
||||
command: "rm -rf tmp".to_string(),
|
||||
cwd: "/tmp".into(),
|
||||
},
|
||||
}
|
||||
assessment_event.target_item_id.as_deref(),
|
||||
Some("command-item-1")
|
||||
);
|
||||
assert_eq!(assessment_event.turn_id, parent_ctx.sub_id);
|
||||
assert_eq!(
|
||||
assessment_event.status,
|
||||
GuardianAssessmentStatus::InProgress
|
||||
);
|
||||
assert_eq!(assessment_event.risk_level, None);
|
||||
assert_eq!(assessment_event.user_authorization, None);
|
||||
assert_eq!(assessment_event.rationale, None);
|
||||
assert_eq!(assessment_event.decision_source, None);
|
||||
assert_eq!(assessment_event.action, expected_action);
|
||||
|
||||
cancel_token.cancel();
|
||||
|
||||
|
||||
@@ -2012,7 +2012,6 @@ async fn set_rate_limits_retains_previous_credits() {
|
||||
unlimited: false,
|
||||
balance: Some("10.00".to_string()),
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: Some(codex_protocol::account::PlanType::Plus),
|
||||
};
|
||||
state.set_rate_limits(initial.clone());
|
||||
@@ -2031,7 +2030,6 @@ async fn set_rate_limits_retains_previous_credits() {
|
||||
resets_at: Some(1_900),
|
||||
}),
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
};
|
||||
state.set_rate_limits(update.clone());
|
||||
@@ -2044,7 +2042,6 @@ async fn set_rate_limits_retains_previous_credits() {
|
||||
primary: update.primary.clone(),
|
||||
secondary: update.secondary,
|
||||
credits: initial.credits,
|
||||
spend_control: None,
|
||||
plan_type: initial.plan_type,
|
||||
})
|
||||
);
|
||||
@@ -2121,7 +2118,6 @@ async fn set_rate_limits_updates_plan_type_when_present() {
|
||||
unlimited: false,
|
||||
balance: Some("15.00".to_string()),
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: Some(codex_protocol::account::PlanType::Plus),
|
||||
};
|
||||
state.set_rate_limits(initial.clone());
|
||||
@@ -2136,7 +2132,6 @@ async fn set_rate_limits_updates_plan_type_when_present() {
|
||||
}),
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: Some(codex_protocol::account::PlanType::Pro),
|
||||
};
|
||||
state.set_rate_limits(update.clone());
|
||||
@@ -2149,7 +2144,6 @@ async fn set_rate_limits_updates_plan_type_when_present() {
|
||||
primary: update.primary,
|
||||
secondary: update.secondary,
|
||||
credits: initial.credits,
|
||||
spend_control: None,
|
||||
plan_type: update.plan_type,
|
||||
})
|
||||
);
|
||||
@@ -2879,7 +2873,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
session_telemetry: session_telemetry.clone(),
|
||||
models_manager: Arc::clone(&models_manager),
|
||||
tool_approvals: Mutex::new(ApprovalStore::default()),
|
||||
guardian_rejection_rationales: Mutex::new(std::collections::HashMap::new()),
|
||||
guardian_rejections: Mutex::new(std::collections::HashMap::new()),
|
||||
skills_manager,
|
||||
plugins_manager,
|
||||
mcp_manager,
|
||||
@@ -3724,7 +3718,7 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx(
|
||||
session_telemetry: session_telemetry.clone(),
|
||||
models_manager: Arc::clone(&models_manager),
|
||||
tool_approvals: Mutex::new(ApprovalStore::default()),
|
||||
guardian_rejection_rationales: Mutex::new(std::collections::HashMap::new()),
|
||||
guardian_rejections: Mutex::new(std::collections::HashMap::new()),
|
||||
skills_manager,
|
||||
plugins_manager,
|
||||
mcp_manager,
|
||||
|
||||
@@ -650,14 +650,8 @@ fn windows_restricted_token_supports_full_read_split_write_read_carveouts() {
|
||||
},
|
||||
]);
|
||||
|
||||
#[cfg(windows)]
|
||||
let expected_deny_write_paths = vec![
|
||||
codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(cwd.join(".codex"))
|
||||
.expect("absolute .codex"),
|
||||
codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(&docs)
|
||||
.expect("absolute docs"),
|
||||
];
|
||||
#[cfg(not(windows))]
|
||||
// The legacy workspace-write root already protects top-level `.codex`, so
|
||||
// the restricted-token overlay only needs the extra read-only docs carveout.
|
||||
let expected_deny_write_paths = vec![
|
||||
codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(&docs)
|
||||
.expect("absolute docs"),
|
||||
|
||||
@@ -359,15 +359,15 @@ pub(crate) fn guardian_assessment_action(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn guardian_request_id(request: &GuardianApprovalRequest) -> &str {
|
||||
pub(crate) fn guardian_request_target_item_id(request: &GuardianApprovalRequest) -> Option<&str> {
|
||||
match request {
|
||||
GuardianApprovalRequest::Shell { id, .. }
|
||||
| GuardianApprovalRequest::ExecCommand { id, .. }
|
||||
| GuardianApprovalRequest::ApplyPatch { id, .. }
|
||||
| GuardianApprovalRequest::NetworkAccess { id, .. }
|
||||
| GuardianApprovalRequest::McpToolCall { id, .. } => id,
|
||||
| GuardianApprovalRequest::McpToolCall { id, .. } => Some(id),
|
||||
GuardianApprovalRequest::NetworkAccess { .. } => None,
|
||||
#[cfg(unix)]
|
||||
GuardianApprovalRequest::Execve { id, .. } => id,
|
||||
GuardianApprovalRequest::Execve { id, .. } => Some(id),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ mod review_session;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use codex_protocol::protocol::GuardianAssessmentDecisionSource;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -29,6 +30,7 @@ pub(crate) use review::GuardianApprovalReviewResult;
|
||||
pub(crate) use review::GuardianApprovalReviewStatus;
|
||||
pub(crate) use review::guardian_rejection_message;
|
||||
pub(crate) use review::is_guardian_reviewer_source;
|
||||
pub(crate) use review::new_guardian_review_id;
|
||||
pub(crate) use review::review_approval_request;
|
||||
pub(crate) use review::review_approval_request_with_cancel;
|
||||
pub(crate) use review::review_approval_request_with_review;
|
||||
@@ -63,6 +65,12 @@ pub(crate) struct GuardianAssessment {
|
||||
pub(crate) rationale: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct GuardianRejection {
|
||||
pub(crate) rationale: String,
|
||||
pub(crate) source: GuardianAssessmentDecisionSource,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
use approval_request::format_guardian_action_pretty;
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::sync::Arc;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::GuardianAssessmentDecisionSource;
|
||||
use codex_protocol::protocol::GuardianAssessmentEvent;
|
||||
use codex_protocol::protocol::GuardianAssessmentStatus;
|
||||
use codex_protocol::protocol::GuardianRiskLevel;
|
||||
@@ -19,8 +20,9 @@ use super::GUARDIAN_REVIEWER_NAME;
|
||||
use super::GuardianApprovalRequest;
|
||||
use super::GuardianAssessment;
|
||||
use super::GuardianAssessmentOutcome;
|
||||
use super::GuardianRejection;
|
||||
use super::approval_request::guardian_assessment_action;
|
||||
use super::approval_request::guardian_request_id;
|
||||
use super::approval_request::guardian_request_target_item_id;
|
||||
use super::approval_request::guardian_request_turn_id;
|
||||
use super::prompt::guardian_output_schema;
|
||||
use super::prompt::parse_guardian_assessment;
|
||||
@@ -36,20 +38,29 @@ const GUARDIAN_REJECTION_INSTRUCTIONS: &str = concat!(
|
||||
"Otherwise, stop and request user input.",
|
||||
);
|
||||
|
||||
pub(crate) async fn guardian_rejection_message(session: &Session, assessment_id: &str) -> String {
|
||||
let rationale = session
|
||||
pub(crate) fn new_guardian_review_id() -> String {
|
||||
uuid::Uuid::new_v4().to_string()
|
||||
}
|
||||
|
||||
pub(crate) async fn guardian_rejection_message(session: &Session, review_id: &str) -> String {
|
||||
let rejection = session
|
||||
.services
|
||||
.guardian_rejection_rationales
|
||||
.guardian_rejections
|
||||
.lock()
|
||||
.await
|
||||
.remove(assessment_id)
|
||||
.filter(|rationale| !rationale.trim().is_empty())
|
||||
.unwrap_or_else(|| "Guardian denied the action without a specific rationale.".to_string());
|
||||
format!(
|
||||
"This action was rejected due to unacceptable risk.\nReason: {}\n{}",
|
||||
rationale.trim(),
|
||||
GUARDIAN_REJECTION_INSTRUCTIONS
|
||||
)
|
||||
.remove(review_id)
|
||||
.filter(|rejection| !rejection.rationale.trim().is_empty())
|
||||
.unwrap_or_else(|| GuardianRejection {
|
||||
rationale: "Guardian denied the action without a specific rationale.".to_string(),
|
||||
source: GuardianAssessmentDecisionSource::Agent,
|
||||
});
|
||||
match rejection.source {
|
||||
GuardianAssessmentDecisionSource::Agent => format!(
|
||||
"This action was rejected due to unacceptable risk.\nReason: {}\n{}",
|
||||
rejection.rationale.trim(),
|
||||
GUARDIAN_REJECTION_INSTRUCTIONS
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -115,23 +126,26 @@ pub(crate) fn is_guardian_reviewer_source(
|
||||
async fn run_guardian_review(
|
||||
session: Arc<Session>,
|
||||
turn: Arc<TurnContext>,
|
||||
review_id: String,
|
||||
request: GuardianApprovalRequest,
|
||||
retry_reason: Option<String>,
|
||||
external_cancel: Option<CancellationToken>,
|
||||
) -> GuardianApprovalReviewResult {
|
||||
let assessment_id = guardian_request_id(&request).to_string();
|
||||
let target_item_id = guardian_request_target_item_id(&request).map(str::to_string);
|
||||
let assessment_turn_id = guardian_request_turn_id(&request, &turn.sub_id).to_string();
|
||||
let action_summary = guardian_assessment_action(&request);
|
||||
session
|
||||
.send_event(
|
||||
turn.as_ref(),
|
||||
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||
id: assessment_id.clone(),
|
||||
id: review_id.clone(),
|
||||
target_item_id: target_item_id.clone(),
|
||||
turn_id: assessment_turn_id.clone(),
|
||||
status: GuardianAssessmentStatus::InProgress,
|
||||
risk_level: None,
|
||||
user_authorization: None,
|
||||
rationale: None,
|
||||
decision_source: None,
|
||||
action: action_summary.clone(),
|
||||
}),
|
||||
)
|
||||
@@ -145,12 +159,14 @@ async fn run_guardian_review(
|
||||
.send_event(
|
||||
turn.as_ref(),
|
||||
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||
id: assessment_id,
|
||||
id: review_id,
|
||||
target_item_id,
|
||||
turn_id: assessment_turn_id,
|
||||
status: GuardianAssessmentStatus::Aborted,
|
||||
risk_level: None,
|
||||
user_authorization: None,
|
||||
rationale: None,
|
||||
decision_source: Some(GuardianAssessmentDecisionSource::Agent),
|
||||
action: action_summary,
|
||||
}),
|
||||
)
|
||||
@@ -206,12 +222,14 @@ async fn run_guardian_review(
|
||||
.send_event(
|
||||
turn.as_ref(),
|
||||
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||
id: assessment_id,
|
||||
id: review_id,
|
||||
target_item_id,
|
||||
turn_id: assessment_turn_id,
|
||||
status: GuardianAssessmentStatus::Aborted,
|
||||
risk_level: None,
|
||||
user_authorization: None,
|
||||
rationale: None,
|
||||
decision_source: Some(GuardianAssessmentDecisionSource::Agent),
|
||||
action: action_summary,
|
||||
}),
|
||||
)
|
||||
@@ -274,23 +292,29 @@ async fn run_guardian_review(
|
||||
rationale: Some(assessment.rationale.clone()),
|
||||
};
|
||||
{
|
||||
let mut rationales = session.services.guardian_rejection_rationales.lock().await;
|
||||
let mut rationales = session.services.guardian_rejections.lock().await;
|
||||
if approved {
|
||||
rationales.remove(&assessment_id);
|
||||
rationales.remove(&review_id);
|
||||
} else {
|
||||
rationales.insert(assessment_id.clone(), assessment.rationale.clone());
|
||||
let rejection = GuardianRejection {
|
||||
rationale: assessment.rationale.clone(),
|
||||
source: GuardianAssessmentDecisionSource::Agent,
|
||||
};
|
||||
rationales.insert(review_id.clone(), rejection);
|
||||
}
|
||||
}
|
||||
session
|
||||
.send_event(
|
||||
turn.as_ref(),
|
||||
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||
id: assessment_id,
|
||||
id: review_id,
|
||||
target_item_id,
|
||||
turn_id: assessment_turn_id,
|
||||
status,
|
||||
risk_level: Some(assessment.risk_level),
|
||||
user_authorization: Some(assessment.user_authorization),
|
||||
rationale: Some(assessment.rationale.clone()),
|
||||
decision_source: Some(GuardianAssessmentDecisionSource::Agent),
|
||||
action: terminal_action,
|
||||
}),
|
||||
)
|
||||
@@ -303,12 +327,14 @@ async fn run_guardian_review(
|
||||
pub(crate) async fn review_approval_request_with_review(
|
||||
session: &Arc<Session>,
|
||||
turn: &Arc<TurnContext>,
|
||||
review_id: String,
|
||||
request: GuardianApprovalRequest,
|
||||
retry_reason: Option<String>,
|
||||
) -> GuardianApprovalReviewResult {
|
||||
run_guardian_review(
|
||||
Arc::clone(session),
|
||||
Arc::clone(turn),
|
||||
review_id,
|
||||
request,
|
||||
retry_reason,
|
||||
/*external_cancel*/ None,
|
||||
@@ -320,10 +346,11 @@ pub(crate) async fn review_approval_request_with_review(
|
||||
pub(crate) async fn review_approval_request(
|
||||
session: &Arc<Session>,
|
||||
turn: &Arc<TurnContext>,
|
||||
review_id: String,
|
||||
request: GuardianApprovalRequest,
|
||||
retry_reason: Option<String>,
|
||||
) -> ReviewDecision {
|
||||
review_approval_request_with_review(session, turn, request, retry_reason)
|
||||
review_approval_request_with_review(session, turn, review_id, request, retry_reason)
|
||||
.await
|
||||
.decision
|
||||
}
|
||||
@@ -331,6 +358,7 @@ pub(crate) async fn review_approval_request(
|
||||
pub(crate) async fn review_approval_request_with_cancel(
|
||||
session: &Arc<Session>,
|
||||
turn: &Arc<TurnContext>,
|
||||
review_id: String,
|
||||
request: GuardianApprovalRequest,
|
||||
retry_reason: Option<String>,
|
||||
cancel_token: CancellationToken,
|
||||
@@ -338,6 +366,7 @@ pub(crate) async fn review_approval_request_with_cancel(
|
||||
run_guardian_review(
|
||||
Arc::clone(session),
|
||||
Arc::clone(turn),
|
||||
review_id,
|
||||
request,
|
||||
retry_reason,
|
||||
Some(cancel_token),
|
||||
|
||||
@@ -414,7 +414,7 @@ impl GuardianReviewSessionManager {
|
||||
let snapshot = state.last_committed_fork_snapshot.as_ref()?;
|
||||
match &snapshot.initial_history {
|
||||
InitialHistory::Forked(items) => Some(items.clone()),
|
||||
InitialHistory::New | InitialHistory::Resumed(_) => None,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -683,6 +683,7 @@ async fn cancelled_guardian_review_emits_terminal_abort_without_warning() {
|
||||
let decision = review_approval_request_with_cancel(
|
||||
&session,
|
||||
&turn,
|
||||
"review-cancelled-guardian".to_string(),
|
||||
GuardianApprovalRequest::ApplyPatch {
|
||||
id: "patch-1".to_string(),
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
@@ -1248,6 +1249,7 @@ async fn guardian_review_surfaces_responses_api_errors_in_rejection_reason() ->
|
||||
let decision = review_approval_request(
|
||||
&session,
|
||||
&turn,
|
||||
"review-shell-guardian-error".to_string(),
|
||||
GuardianApprovalRequest::Shell {
|
||||
id: "shell-guardian-error".to_string(),
|
||||
command: vec!["git".to_string(), "push".to_string()],
|
||||
@@ -1295,8 +1297,13 @@ async fn guardian_review_surfaces_responses_api_errors_in_rejection_reason() ->
|
||||
}),
|
||||
"denial rationale should not fall back to the generic missing payload error"
|
||||
);
|
||||
{
|
||||
let rationales = session.services.guardian_rejections.lock().await;
|
||||
assert!(rationales.contains_key("review-shell-guardian-error"));
|
||||
assert!(!rationales.contains_key("shell-guardian-error"));
|
||||
}
|
||||
let rejection_message =
|
||||
guardian_rejection_message(session.as_ref(), "shell-guardian-error").await;
|
||||
guardian_rejection_message(session.as_ref(), "review-shell-guardian-error").await;
|
||||
assert!(
|
||||
rejection_message.contains("Reason: Automatic approval review failed:")
|
||||
&& rejection_message.contains(error_message),
|
||||
@@ -1375,7 +1382,14 @@ async fn guardian_parallel_reviews_fork_from_last_committed_trunk_history() -> a
|
||||
justification: Some("Inspect repo state before proceeding.".to_string()),
|
||||
};
|
||||
assert_eq!(
|
||||
review_approval_request(&session, &turn, initial_request, /*retry_reason*/ None).await,
|
||||
review_approval_request(
|
||||
&session,
|
||||
&turn,
|
||||
"review-shell-guardian-1".to_string(),
|
||||
initial_request,
|
||||
/*retry_reason*/ None
|
||||
)
|
||||
.await,
|
||||
ReviewDecision::Approved
|
||||
);
|
||||
session
|
||||
@@ -1427,6 +1441,7 @@ async fn guardian_parallel_reviews_fork_from_last_committed_trunk_history() -> a
|
||||
review_approval_request(
|
||||
&session_for_second,
|
||||
&turn_for_second,
|
||||
"review-shell-guardian-2".to_string(),
|
||||
second_request,
|
||||
Some("trunk follow-up".to_string()),
|
||||
)
|
||||
@@ -1475,6 +1490,7 @@ async fn guardian_parallel_reviews_fork_from_last_committed_trunk_history() -> a
|
||||
let third_decision = review_approval_request(
|
||||
&session,
|
||||
&turn,
|
||||
"review-shell-guardian-3".to_string(),
|
||||
third_request,
|
||||
Some("parallel follow-up".to_string()),
|
||||
)
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
// the TUI or the tracing stack).
|
||||
#![deny(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
mod account;
|
||||
mod apply_patch;
|
||||
mod apps;
|
||||
mod arc_monitor;
|
||||
@@ -113,13 +112,7 @@ mod stream_events_utils;
|
||||
pub mod test_support;
|
||||
mod unified_exec;
|
||||
pub mod windows_sandbox;
|
||||
pub use account::resolve_workspace_role_and_owner_for_auth;
|
||||
pub use account::send_add_credits_nudge_email;
|
||||
pub use client::X_RESPONSESAPI_INCLUDE_TIMING_METRICS_HEADER;
|
||||
pub use codex_account::AddCreditsNudgeEmailStatus;
|
||||
pub use codex_account::SendAddCreditsNudgeEmailError;
|
||||
pub use codex_account::WorkspaceOwnership;
|
||||
pub use codex_account::WorkspaceRole;
|
||||
pub use codex_protocol::config_types::ModelProviderAuthInfo;
|
||||
mod event_mapping;
|
||||
pub mod review_format;
|
||||
|
||||
@@ -24,6 +24,7 @@ use crate::guardian::GuardianApprovalRequest;
|
||||
use crate::guardian::GuardianMcpAnnotations;
|
||||
use crate::guardian::guardian_approval_request_to_json;
|
||||
use crate::guardian::guardian_rejection_message;
|
||||
use crate::guardian::new_guardian_review_id;
|
||||
use crate::guardian::review_approval_request;
|
||||
use crate::guardian::routes_approval_to_guardian;
|
||||
use crate::mcp_openai_file::rewrite_mcp_tool_arguments_for_openai_files;
|
||||
@@ -772,14 +773,16 @@ async fn maybe_request_mcp_tool_approval(
|
||||
.enabled(Feature::ToolCallMcpElicitation);
|
||||
|
||||
if routes_approval_to_guardian(turn_context) {
|
||||
let review_id = new_guardian_review_id();
|
||||
let decision = review_approval_request(
|
||||
sess,
|
||||
turn_context,
|
||||
review_id.clone(),
|
||||
build_guardian_mcp_tool_review_request(call_id, invocation, metadata),
|
||||
monitor_reason.clone(),
|
||||
)
|
||||
.await;
|
||||
let decision = mcp_tool_approval_decision_from_guardian(sess, call_id, decision).await;
|
||||
let decision = mcp_tool_approval_decision_from_guardian(sess, &review_id, decision).await;
|
||||
apply_mcp_tool_approval_decision(
|
||||
sess,
|
||||
turn_context,
|
||||
@@ -969,7 +972,7 @@ pub(crate) fn build_guardian_mcp_tool_review_request(
|
||||
|
||||
async fn mcp_tool_approval_decision_from_guardian(
|
||||
sess: &Session,
|
||||
call_id: &str,
|
||||
review_id: &str,
|
||||
decision: ReviewDecision,
|
||||
) -> McpToolApprovalDecision {
|
||||
match decision {
|
||||
@@ -978,7 +981,7 @@ async fn mcp_tool_approval_decision_from_guardian(
|
||||
| ReviewDecision::NetworkPolicyAmendment { .. } => McpToolApprovalDecision::Accept,
|
||||
ReviewDecision::ApprovedForSession => McpToolApprovalDecision::AcceptForSession,
|
||||
ReviewDecision::Denied => McpToolApprovalDecision::Decline {
|
||||
message: Some(guardian_rejection_message(sess, call_id).await),
|
||||
message: Some(guardian_rejection_message(sess, review_id).await),
|
||||
},
|
||||
ReviewDecision::Abort => McpToolApprovalDecision::Decline { message: None },
|
||||
}
|
||||
|
||||
@@ -816,21 +816,22 @@ async fn guardian_review_decision_maps_to_mcp_tool_decision() {
|
||||
assert_eq!(
|
||||
mcp_tool_approval_decision_from_guardian(
|
||||
session.as_ref(),
|
||||
"approval-id",
|
||||
"review-id",
|
||||
ReviewDecision::Approved
|
||||
)
|
||||
.await,
|
||||
McpToolApprovalDecision::Accept
|
||||
);
|
||||
session
|
||||
.services
|
||||
.guardian_rejection_rationales
|
||||
.lock()
|
||||
.await
|
||||
.insert("approval-id".to_string(), "too risky".to_string());
|
||||
session.services.guardian_rejections.lock().await.insert(
|
||||
"review-id".to_string(),
|
||||
crate::guardian::GuardianRejection {
|
||||
rationale: "too risky".to_string(),
|
||||
source: codex_protocol::protocol::GuardianAssessmentDecisionSource::Agent,
|
||||
},
|
||||
);
|
||||
let denial = mcp_tool_approval_decision_from_guardian(
|
||||
session.as_ref(),
|
||||
"approval-id",
|
||||
"review-id",
|
||||
ReviewDecision::Denied,
|
||||
)
|
||||
.await;
|
||||
@@ -845,7 +846,7 @@ async fn guardian_review_decision_maps_to_mcp_tool_decision() {
|
||||
assert_eq!(
|
||||
mcp_tool_approval_decision_from_guardian(
|
||||
session.as_ref(),
|
||||
"approval-id",
|
||||
"review-id",
|
||||
ReviewDecision::Abort
|
||||
)
|
||||
.await,
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::agent::AgentControl;
|
||||
use crate::client::ModelClient;
|
||||
use crate::config::StartedNetworkProxy;
|
||||
use crate::exec_policy::ExecPolicyManager;
|
||||
use crate::guardian::GuardianRejection;
|
||||
use crate::mcp::McpManager;
|
||||
use crate::plugins::PluginsManager;
|
||||
use crate::skills_watcher::SkillsWatcher;
|
||||
@@ -47,7 +48,7 @@ pub(crate) struct SessionServices {
|
||||
pub(crate) models_manager: Arc<ModelsManager>,
|
||||
pub(crate) session_telemetry: SessionTelemetry,
|
||||
pub(crate) tool_approvals: Mutex<ApprovalStore>,
|
||||
pub(crate) guardian_rejection_rationales: Mutex<HashMap<String, String>>,
|
||||
pub(crate) guardian_rejections: Mutex<HashMap<String, GuardianRejection>>,
|
||||
pub(crate) skills_manager: Arc<SkillsManager>,
|
||||
pub(crate) plugins_manager: Arc<PluginsManager>,
|
||||
pub(crate) mcp_manager: Arc<McpManager>,
|
||||
|
||||
@@ -229,9 +229,6 @@ fn merge_rate_limit_fields(
|
||||
if snapshot.credits.is_none() {
|
||||
snapshot.credits = previous.and_then(|prior| prior.credits.clone());
|
||||
}
|
||||
if snapshot.spend_control.is_none() {
|
||||
snapshot.spend_control = previous.and_then(|prior| prior.spend_control.clone());
|
||||
}
|
||||
if snapshot.plan_type.is_none() {
|
||||
snapshot.plan_type = previous.and_then(|prior| prior.plan_type);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,6 @@ async fn set_rate_limits_defaults_limit_id_to_codex_when_missing() {
|
||||
}),
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
});
|
||||
|
||||
@@ -76,7 +75,6 @@ async fn set_rate_limits_defaults_to_codex_when_limit_id_missing_after_other_buc
|
||||
}),
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
});
|
||||
state.set_rate_limits(RateLimitSnapshot {
|
||||
@@ -89,7 +87,6 @@ async fn set_rate_limits_defaults_to_codex_when_limit_id_missing_after_other_buc
|
||||
}),
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
});
|
||||
|
||||
@@ -121,7 +118,6 @@ async fn set_rate_limits_carries_credits_and_plan_type_from_codex_to_codex_other
|
||||
unlimited: false,
|
||||
balance: Some("50".to_string()),
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: Some(codex_protocol::account::PlanType::Plus),
|
||||
});
|
||||
|
||||
@@ -135,7 +131,6 @@ async fn set_rate_limits_carries_credits_and_plan_type_from_codex_to_codex_other
|
||||
}),
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
});
|
||||
|
||||
@@ -155,7 +150,6 @@ async fn set_rate_limits_carries_credits_and_plan_type_from_codex_to_codex_other
|
||||
unlimited: false,
|
||||
balance: Some("50".to_string()),
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: Some(codex_protocol::account::PlanType::Plus),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -474,6 +474,7 @@ impl ThreadManager {
|
||||
) -> CodexResult<NewThread> {
|
||||
Box::pin(self.start_thread_with_tools_and_service_name(
|
||||
config,
|
||||
InitialHistory::New,
|
||||
dynamic_tools,
|
||||
persist_extended_history,
|
||||
/*metrics_service_name*/ None,
|
||||
@@ -485,6 +486,7 @@ impl ThreadManager {
|
||||
pub async fn start_thread_with_tools_and_service_name(
|
||||
&self,
|
||||
config: Config,
|
||||
initial_history: InitialHistory,
|
||||
dynamic_tools: Vec<codex_protocol::dynamic_tools::DynamicToolSpec>,
|
||||
persist_extended_history: bool,
|
||||
metrics_service_name: Option<String>,
|
||||
@@ -492,7 +494,7 @@ impl ThreadManager {
|
||||
) -> CodexResult<NewThread> {
|
||||
Box::pin(self.state.spawn_thread(
|
||||
config,
|
||||
InitialHistory::New,
|
||||
initial_history,
|
||||
Arc::clone(&self.state.auth_manager),
|
||||
self.agent_control(),
|
||||
dynamic_tools,
|
||||
@@ -668,6 +670,7 @@ impl ThreadManager {
|
||||
ForkSnapshot::Interrupted => {
|
||||
let history = match history {
|
||||
InitialHistory::New => InitialHistory::New,
|
||||
InitialHistory::Cleared => InitialHistory::Cleared,
|
||||
InitialHistory::Forked(history) => InitialHistory::Forked(history),
|
||||
InitialHistory::Resumed(resumed) => InitialHistory::Forked(resumed.history),
|
||||
};
|
||||
@@ -1070,7 +1073,7 @@ fn append_interrupted_boundary(history: InitialHistory, turn_id: Option<String>)
|
||||
}));
|
||||
|
||||
match history {
|
||||
InitialHistory::New => InitialHistory::Forked(vec![
|
||||
InitialHistory::New | InitialHistory::Cleared => InitialHistory::Forked(vec![
|
||||
RolloutItem::ResponseItem(interrupted_turn_history_marker()),
|
||||
aborted_event,
|
||||
]),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::codex::Session;
|
||||
use crate::guardian::GuardianApprovalRequest;
|
||||
use crate::guardian::guardian_rejection_message;
|
||||
use crate::guardian::new_guardian_review_id;
|
||||
use crate::guardian::review_approval_request;
|
||||
use crate::guardian::routes_approval_to_guardian;
|
||||
use crate::network_policy_decision::denied_network_policy_message;
|
||||
@@ -369,12 +370,13 @@ impl NetworkApprovalService {
|
||||
};
|
||||
let owner_call = self.resolve_single_active_call().await;
|
||||
let guardian_approval_id = Self::approval_id_for_key(&key);
|
||||
let approval_decision = if routes_approval_to_guardian(&turn_context) {
|
||||
// TODO(ccunningham): Attach guardian network reviews to the reviewed tool item
|
||||
// lifecycle instead of this temporary standalone network approval id.
|
||||
let use_guardian = routes_approval_to_guardian(&turn_context);
|
||||
let guardian_review_id = use_guardian.then(new_guardian_review_id);
|
||||
let approval_decision = if let Some(review_id) = guardian_review_id.clone() {
|
||||
review_approval_request(
|
||||
&session,
|
||||
&turn_context,
|
||||
review_id,
|
||||
GuardianApprovalRequest::NetworkAccess {
|
||||
id: guardian_approval_id.clone(),
|
||||
turn_id: owner_call
|
||||
@@ -487,11 +489,9 @@ impl NetworkApprovalService {
|
||||
}
|
||||
},
|
||||
ReviewDecision::Denied | ReviewDecision::Abort => {
|
||||
if routes_approval_to_guardian(&turn_context) {
|
||||
if let Some(review_id) = guardian_review_id.as_deref() {
|
||||
if let Some(owner_call) = owner_call.as_ref() {
|
||||
let message =
|
||||
guardian_rejection_message(session.as_ref(), &guardian_approval_id)
|
||||
.await;
|
||||
let message = guardian_rejection_message(session.as_ref(), review_id).await;
|
||||
self.record_call_outcome(
|
||||
&owner_call.registration_id,
|
||||
NetworkApprovalOutcome::DeniedByPolicy(message),
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::guardian::GuardianApprovalReviewResult;
|
||||
use crate::guardian::GuardianApprovalReviewStatus;
|
||||
use crate::guardian::GuardianAssessmentOutcome;
|
||||
use crate::guardian::guardian_rejection_message;
|
||||
use crate::guardian::new_guardian_review_id;
|
||||
use crate::guardian::review_approval_request_with_review;
|
||||
use crate::guardian::routes_approval_to_guardian;
|
||||
use crate::hook_runtime::run_permission_request_hooks;
|
||||
@@ -123,6 +124,7 @@ impl ToolOrchestrator {
|
||||
let otel = turn_ctx.session_telemetry.clone();
|
||||
let otel_tn = &tool_ctx.tool_name;
|
||||
let otel_ci = &tool_ctx.call_id;
|
||||
let use_guardian = routes_approval_to_guardian(turn_ctx);
|
||||
// 1) Approval
|
||||
let mut already_approved = false;
|
||||
|
||||
@@ -142,10 +144,12 @@ impl ToolOrchestrator {
|
||||
return Err(ToolError::Rejected(reason));
|
||||
}
|
||||
ExecApprovalRequirement::NeedsApproval { reason, .. } => {
|
||||
let guardian_review_id = use_guardian.then(new_guardian_review_id);
|
||||
let approval_ctx = ApprovalCtx {
|
||||
session: &tool_ctx.session,
|
||||
turn: &tool_ctx.turn,
|
||||
call_id: &tool_ctx.call_id,
|
||||
guardian_review_id: guardian_review_id.clone(),
|
||||
retry_reason: reason,
|
||||
network_approval_context: None,
|
||||
};
|
||||
@@ -162,9 +166,8 @@ impl ToolOrchestrator {
|
||||
|
||||
match decision {
|
||||
ReviewDecision::Denied | ReviewDecision::Abort => {
|
||||
let reason = if routes_approval_to_guardian(turn_ctx) {
|
||||
guardian_rejection_message(tool_ctx.session.as_ref(), &tool_ctx.call_id)
|
||||
.await
|
||||
let reason = if let Some(review_id) = guardian_review_id.as_deref() {
|
||||
guardian_rejection_message(tool_ctx.session.as_ref(), review_id).await
|
||||
} else {
|
||||
"rejected by user".to_string()
|
||||
};
|
||||
@@ -298,14 +301,15 @@ impl ToolOrchestrator {
|
||||
.should_bypass_approval(approval_policy, already_approved)
|
||||
&& network_approval_context.is_none();
|
||||
if !bypass_retry_approval {
|
||||
let guardian_review_id = use_guardian.then(new_guardian_review_id);
|
||||
let approval_ctx = ApprovalCtx {
|
||||
session: &tool_ctx.session,
|
||||
turn: &tool_ctx.turn,
|
||||
call_id: &tool_ctx.call_id,
|
||||
guardian_review_id: guardian_review_id.clone(),
|
||||
retry_reason: Some(retry_reason),
|
||||
network_approval_context: network_approval_context.clone(),
|
||||
};
|
||||
|
||||
let decision = Self::request_approval(
|
||||
tool,
|
||||
req,
|
||||
@@ -319,12 +323,9 @@ impl ToolOrchestrator {
|
||||
|
||||
match decision {
|
||||
ReviewDecision::Denied | ReviewDecision::Abort => {
|
||||
let reason = if routes_approval_to_guardian(turn_ctx) {
|
||||
guardian_rejection_message(
|
||||
tool_ctx.session.as_ref(),
|
||||
&tool_ctx.call_id,
|
||||
)
|
||||
.await
|
||||
let reason = if let Some(review_id) = guardian_review_id.as_deref() {
|
||||
guardian_rejection_message(tool_ctx.session.as_ref(), review_id)
|
||||
.await
|
||||
} else {
|
||||
"rejected by user".to_string()
|
||||
};
|
||||
@@ -403,6 +404,10 @@ impl ToolOrchestrator {
|
||||
review_approval_request_with_review(
|
||||
approval_ctx.session,
|
||||
approval_ctx.turn,
|
||||
approval_ctx
|
||||
.guardian_review_id
|
||||
.clone()
|
||||
.expect("guardian review id should be present for guardian approvals"),
|
||||
request,
|
||||
approval_ctx.retry_reason.clone(),
|
||||
)
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
use crate::exec::ExecCapturePolicy;
|
||||
use crate::guardian::GuardianApprovalRequest;
|
||||
use crate::guardian::review_approval_request;
|
||||
use crate::guardian::routes_approval_to_guardian;
|
||||
use crate::sandboxing::ExecOptions;
|
||||
use crate::sandboxing::execute_env;
|
||||
use crate::tools::sandboxing::Approvable;
|
||||
@@ -147,13 +146,15 @@ impl Approvable<ApplyPatchRequest> for ApplyPatchRuntime {
|
||||
let retry_reason = ctx.retry_reason.clone();
|
||||
let approval_keys = self.approval_keys(req);
|
||||
let changes = req.changes.clone();
|
||||
let guardian_review_id = ctx.guardian_review_id.clone();
|
||||
Box::pin(async move {
|
||||
if req.permissions_preapproved && retry_reason.is_none() {
|
||||
return ReviewDecision::Approved;
|
||||
}
|
||||
if routes_approval_to_guardian(turn) {
|
||||
if let Some(review_id) = guardian_review_id {
|
||||
let action = ApplyPatchRuntime::build_guardian_review_request(req, ctx.call_id);
|
||||
return review_approval_request(session, turn, action, retry_reason).await;
|
||||
return review_approval_request(session, turn, review_id, action, retry_reason)
|
||||
.await;
|
||||
}
|
||||
if let Some(reason) = retry_reason {
|
||||
let rx_approve = session
|
||||
|
||||
@@ -12,7 +12,6 @@ use crate::command_canonicalization::canonicalize_command_for_approval;
|
||||
use crate::exec::ExecCapturePolicy;
|
||||
use crate::guardian::GuardianApprovalRequest;
|
||||
use crate::guardian::review_approval_request;
|
||||
use crate::guardian::routes_approval_to_guardian;
|
||||
use crate::sandboxing::ExecOptions;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::sandboxing::execute_env;
|
||||
@@ -166,10 +165,17 @@ impl Approvable<ShellRequest> for ShellRuntime {
|
||||
let session = ctx.session;
|
||||
let turn = ctx.turn;
|
||||
let call_id = ctx.call_id.to_string();
|
||||
let guardian_review_id = ctx.guardian_review_id.clone();
|
||||
Box::pin(async move {
|
||||
if routes_approval_to_guardian(turn) {
|
||||
return review_approval_request(session, turn, guardian_request, retry_reason)
|
||||
.await;
|
||||
if let Some(review_id) = guardian_review_id {
|
||||
return review_approval_request(
|
||||
session,
|
||||
turn,
|
||||
review_id,
|
||||
guardian_request,
|
||||
retry_reason,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
with_cached_approval(&session.services, "shell", keys, move || async move {
|
||||
let available_decisions = None;
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::exec::ExecExpiration;
|
||||
use crate::exec::is_likely_sandbox_denied;
|
||||
use crate::guardian::GuardianApprovalRequest;
|
||||
use crate::guardian::guardian_rejection_message;
|
||||
use crate::guardian::new_guardian_review_id;
|
||||
use crate::guardian::review_approval_request;
|
||||
use crate::guardian::routes_approval_to_guardian;
|
||||
use crate::sandboxing::ExecOptions;
|
||||
@@ -315,6 +316,11 @@ enum DecisionSource {
|
||||
UnmatchedCommandFallback,
|
||||
}
|
||||
|
||||
struct PromptDecision {
|
||||
decision: ReviewDecision,
|
||||
guardian_review_id: Option<String>,
|
||||
}
|
||||
|
||||
fn execve_prompt_is_rejected_by_policy(
|
||||
approval_policy: AskForApproval,
|
||||
decision_source: &DecisionSource,
|
||||
@@ -376,7 +382,7 @@ impl CoreShellActionProvider {
|
||||
workdir: &AbsolutePathBuf,
|
||||
stopwatch: &Stopwatch,
|
||||
additional_permissions: Option<PermissionProfile>,
|
||||
) -> anyhow::Result<ReviewDecision> {
|
||||
) -> anyhow::Result<PromptDecision> {
|
||||
let command = join_program_and_argv(program, argv);
|
||||
let workdir = workdir.to_path_buf();
|
||||
let session = self.session.clone();
|
||||
@@ -384,12 +390,14 @@ impl CoreShellActionProvider {
|
||||
let call_id = self.call_id.clone();
|
||||
let approval_id = Some(Uuid::new_v4().to_string());
|
||||
let source = self.tool_name;
|
||||
let guardian_review_id = routes_approval_to_guardian(&turn).then(new_guardian_review_id);
|
||||
Ok(stopwatch
|
||||
.pause_for(async move {
|
||||
if routes_approval_to_guardian(&turn) {
|
||||
return review_approval_request(
|
||||
if let Some(review_id) = guardian_review_id.clone() {
|
||||
let decision = review_approval_request(
|
||||
&session,
|
||||
&turn,
|
||||
review_id,
|
||||
GuardianApprovalRequest::Execve {
|
||||
id: call_id.clone(),
|
||||
source,
|
||||
@@ -401,8 +409,12 @@ impl CoreShellActionProvider {
|
||||
/*retry_reason*/ None,
|
||||
)
|
||||
.await;
|
||||
return PromptDecision {
|
||||
decision,
|
||||
guardian_review_id,
|
||||
};
|
||||
}
|
||||
session
|
||||
let decision = session
|
||||
.request_command_approval(
|
||||
&turn,
|
||||
call_id,
|
||||
@@ -415,7 +427,11 @@ impl CoreShellActionProvider {
|
||||
additional_permissions,
|
||||
Some(vec![ReviewDecision::Approved, ReviewDecision::Abort]),
|
||||
)
|
||||
.await
|
||||
.await;
|
||||
PromptDecision {
|
||||
decision,
|
||||
guardian_review_id: None,
|
||||
}
|
||||
})
|
||||
.await)
|
||||
}
|
||||
@@ -442,10 +458,10 @@ impl CoreShellActionProvider {
|
||||
{
|
||||
EscalationDecision::deny(Some("Execution forbidden by policy".to_string()))
|
||||
} else {
|
||||
match self
|
||||
let prompt_decision = self
|
||||
.prompt(program, argv, workdir, &self.stopwatch, prompt_permissions)
|
||||
.await?
|
||||
{
|
||||
.await?;
|
||||
match prompt_decision.decision {
|
||||
ReviewDecision::Approved
|
||||
| ReviewDecision::ApprovedForSession
|
||||
| ReviewDecision::ApprovedExecpolicyAmendment { .. } => {
|
||||
@@ -470,9 +486,10 @@ impl CoreShellActionProvider {
|
||||
}
|
||||
},
|
||||
ReviewDecision::Denied => {
|
||||
let message = if routes_approval_to_guardian(&self.turn) {
|
||||
guardian_rejection_message(self.session.as_ref(), &self.call_id)
|
||||
.await
|
||||
let message = if let Some(review_id) =
|
||||
prompt_decision.guardian_review_id.as_deref()
|
||||
{
|
||||
guardian_rejection_message(self.session.as_ref(), review_id).await
|
||||
} else {
|
||||
"User denied execution".to_string()
|
||||
};
|
||||
|
||||
@@ -9,7 +9,6 @@ use crate::exec::ExecCapturePolicy;
|
||||
use crate::exec::ExecExpiration;
|
||||
use crate::guardian::GuardianApprovalRequest;
|
||||
use crate::guardian::review_approval_request;
|
||||
use crate::guardian::routes_approval_to_guardian;
|
||||
use crate::sandboxing::ExecOptions;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::shell::ShellType;
|
||||
@@ -146,10 +145,17 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
|
||||
let cwd = req.cwd.to_path_buf();
|
||||
let retry_reason = ctx.retry_reason.clone();
|
||||
let reason = retry_reason.clone().or_else(|| req.justification.clone());
|
||||
let guardian_review_id = ctx.guardian_review_id.clone();
|
||||
Box::pin(async move {
|
||||
if routes_approval_to_guardian(turn) {
|
||||
return review_approval_request(session, turn, guardian_request, retry_reason)
|
||||
.await;
|
||||
if let Some(review_id) = guardian_review_id {
|
||||
return review_approval_request(
|
||||
session,
|
||||
turn,
|
||||
review_id,
|
||||
guardian_request,
|
||||
retry_reason,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
with_cached_approval(&session.services, "unified_exec", keys, || async move {
|
||||
let available_decisions = None;
|
||||
|
||||
@@ -122,6 +122,13 @@ pub(crate) struct ApprovalCtx<'a> {
|
||||
pub session: &'a Arc<Session>,
|
||||
pub turn: &'a Arc<TurnContext>,
|
||||
pub call_id: &'a str,
|
||||
/// Guardian review lifecycle ID for this approval, when guardian is reviewing it.
|
||||
///
|
||||
/// This is separate from `call_id`: `call_id` identifies the tool item under
|
||||
/// review, while this ID identifies the review itself. Keeping both lets
|
||||
/// denial handling, overrides, and app-server notifications refer to the
|
||||
/// review without overloading the tool call ID as a review ID.
|
||||
pub guardian_review_id: Option<String>,
|
||||
pub retry_reason: Option<String>,
|
||||
pub network_approval_context: Option<NetworkApprovalContext>,
|
||||
}
|
||||
|
||||
@@ -2375,7 +2375,6 @@ async fn token_count_includes_rate_limits_snapshot() {
|
||||
"resets_at": 1704074400
|
||||
},
|
||||
"credits": null,
|
||||
"spend_control": null,
|
||||
"plan_type": null
|
||||
}
|
||||
})
|
||||
@@ -2427,7 +2426,6 @@ async fn token_count_includes_rate_limits_snapshot() {
|
||||
"resets_at": 1704074400
|
||||
},
|
||||
"credits": null,
|
||||
"spend_control": null,
|
||||
"plan_type": null
|
||||
}
|
||||
})
|
||||
@@ -2502,7 +2500,6 @@ async fn usage_limit_error_emits_rate_limit_event() -> anyhow::Result<()> {
|
||||
"resets_at": null
|
||||
},
|
||||
"credits": null,
|
||||
"spend_control": null,
|
||||
"plan_type": null
|
||||
});
|
||||
|
||||
|
||||
@@ -1033,7 +1033,6 @@ async fn responses_websocket_usage_limit_error_emits_rate_limit_event() {
|
||||
"resets_at": null
|
||||
},
|
||||
"credits": null,
|
||||
"spend_control": null,
|
||||
"plan_type": null
|
||||
}
|
||||
})
|
||||
|
||||
@@ -84,6 +84,7 @@ use codex_protocol::protocol::SessionConfiguredEvent;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_absolute_path::canonicalize_existing_preserving_symlinks;
|
||||
use codex_utils_oss::ensure_oss_provider_ready;
|
||||
use codex_utils_oss::get_default_model_for_oss_provider;
|
||||
use event_processor_with_human_output::EventProcessorWithHumanOutput;
|
||||
@@ -278,7 +279,9 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
|
||||
|
||||
let resolved_cwd = cwd.clone();
|
||||
let config_cwd = match resolved_cwd.as_deref() {
|
||||
Some(path) => AbsolutePathBuf::from_absolute_path(path.canonicalize()?)?,
|
||||
Some(path) => {
|
||||
AbsolutePathBuf::from_absolute_path(canonicalize_existing_preserving_symlinks(path)?)?
|
||||
}
|
||||
None => AbsolutePathBuf::current_dir()?,
|
||||
};
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ use crate::schema::SessionStartCommandInput;
|
||||
pub enum SessionStartSource {
|
||||
Startup,
|
||||
Resume,
|
||||
Clear,
|
||||
}
|
||||
|
||||
impl SessionStartSource {
|
||||
@@ -27,6 +28,7 @@ impl SessionStartSource {
|
||||
match self {
|
||||
Self::Startup => "startup",
|
||||
Self::Resume => "resume",
|
||||
Self::Clear => "clear",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
//! - bubblewrap used to construct the filesystem view before exec.
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::path::Path;
|
||||
@@ -273,19 +274,35 @@ fn create_filesystem_args(
|
||||
if !root.exists() {
|
||||
continue;
|
||||
}
|
||||
// Writable roots are rebound by real target below; mirror that
|
||||
// for their restricted-read bootstrap mount. Plain read-only
|
||||
// roots must stay logical because callers may execute those
|
||||
// paths inside bwrap, such as Bazel runfiles helper binaries.
|
||||
let mount_root = if writable_roots
|
||||
.iter()
|
||||
.any(|writable_root| root.starts_with(writable_root.root.as_path()))
|
||||
{
|
||||
canonical_target_if_symlinked_path(&root).unwrap_or(root)
|
||||
} else {
|
||||
root
|
||||
};
|
||||
args.push("--ro-bind".to_string());
|
||||
args.push(path_to_string(&root));
|
||||
args.push(path_to_string(&root));
|
||||
args.push(path_to_string(&mount_root));
|
||||
args.push(path_to_string(&mount_root));
|
||||
}
|
||||
}
|
||||
|
||||
args
|
||||
};
|
||||
let mut preserved_files = Vec::new();
|
||||
let allowed_write_paths: Vec<PathBuf> = writable_roots
|
||||
.iter()
|
||||
.map(|writable_root| writable_root.root.as_path().to_path_buf())
|
||||
.collect();
|
||||
let mut allowed_write_paths = Vec::with_capacity(writable_roots.len());
|
||||
for writable_root in &writable_roots {
|
||||
let root = writable_root.root.as_path();
|
||||
allowed_write_paths.push(root.to_path_buf());
|
||||
if let Some(target) = canonical_target_if_symlinked_path(root) {
|
||||
allowed_write_paths.push(target);
|
||||
}
|
||||
}
|
||||
let unreadable_paths: HashSet<PathBuf> = unreadable_roots
|
||||
.iter()
|
||||
.map(|path| path.as_path().to_path_buf())
|
||||
@@ -321,6 +338,7 @@ fn create_filesystem_args(
|
||||
|
||||
for writable_root in &sorted_writable_roots {
|
||||
let root = writable_root.root.as_path();
|
||||
let symlink_target = canonical_target_if_symlinked_path(root);
|
||||
// If a denied ancestor was already masked, recreate any missing mount
|
||||
// target parents before binding the narrower writable descendant.
|
||||
if let Some(masking_root) = unreadable_roots
|
||||
@@ -332,9 +350,10 @@ fn create_filesystem_args(
|
||||
append_mount_target_parent_dir_args(&mut args, root, masking_root);
|
||||
}
|
||||
|
||||
let mount_root = symlink_target.as_deref().unwrap_or(root);
|
||||
args.push("--bind".to_string());
|
||||
args.push(path_to_string(root));
|
||||
args.push(path_to_string(root));
|
||||
args.push(path_to_string(mount_root));
|
||||
args.push(path_to_string(mount_root));
|
||||
|
||||
let mut read_only_subpaths: Vec<PathBuf> = writable_root
|
||||
.read_only_subpaths
|
||||
@@ -342,6 +361,9 @@ fn create_filesystem_args(
|
||||
.map(|path| path.as_path().to_path_buf())
|
||||
.filter(|path| !unreadable_paths.contains(path))
|
||||
.collect();
|
||||
if let Some(target) = &symlink_target {
|
||||
read_only_subpaths = remap_paths_for_symlink_target(read_only_subpaths, root, target);
|
||||
}
|
||||
read_only_subpaths.sort_by_key(|path| path_depth(path));
|
||||
for subpath in read_only_subpaths {
|
||||
append_read_only_subpath_args(&mut args, &subpath, &allowed_write_paths);
|
||||
@@ -351,6 +373,10 @@ fn create_filesystem_args(
|
||||
.filter(|path| path.as_path().starts_with(root))
|
||||
.map(|path| path.as_path().to_path_buf())
|
||||
.collect();
|
||||
if let Some(target) = &symlink_target {
|
||||
nested_unreadable_roots =
|
||||
remap_paths_for_symlink_target(nested_unreadable_roots, root, target);
|
||||
}
|
||||
nested_unreadable_roots.sort_by_key(|path| path_depth(path));
|
||||
for unreadable_root in nested_unreadable_roots {
|
||||
append_unreadable_root_args(
|
||||
@@ -396,6 +422,52 @@ fn path_depth(path: &Path) -> usize {
|
||||
path.components().count()
|
||||
}
|
||||
|
||||
fn canonical_target_if_symlinked_path(path: &Path) -> Option<PathBuf> {
|
||||
let mut current = PathBuf::new();
|
||||
for component in path.components() {
|
||||
use std::path::Component;
|
||||
match component {
|
||||
Component::RootDir => {
|
||||
current.push(Path::new("/"));
|
||||
continue;
|
||||
}
|
||||
Component::CurDir => continue,
|
||||
Component::ParentDir => {
|
||||
current.pop();
|
||||
continue;
|
||||
}
|
||||
Component::Normal(part) => current.push(part),
|
||||
Component::Prefix(_) => continue,
|
||||
}
|
||||
|
||||
let metadata = match fs::symlink_metadata(¤t) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(_) => return None,
|
||||
};
|
||||
if metadata.file_type().is_symlink() {
|
||||
let target = fs::canonicalize(path).ok()?;
|
||||
if target.as_path() == path {
|
||||
return None;
|
||||
}
|
||||
return Some(target);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn remap_paths_for_symlink_target(paths: Vec<PathBuf>, root: &Path, target: &Path) -> Vec<PathBuf> {
|
||||
paths
|
||||
.into_iter()
|
||||
.map(|path| {
|
||||
if let Ok(relative) = path.strip_prefix(root) {
|
||||
target.join(relative)
|
||||
} else {
|
||||
path
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn normalize_command_cwd_for_bwrap(command_cwd: &Path) -> PathBuf {
|
||||
command_cwd
|
||||
.canonicalize()
|
||||
@@ -427,10 +499,15 @@ fn append_read_only_subpath_args(
|
||||
subpath: &Path,
|
||||
allowed_write_paths: &[PathBuf],
|
||||
) {
|
||||
if let Some(symlink_path) = find_symlink_in_path(subpath, allowed_write_paths) {
|
||||
if let Some(target) = canonical_target_for_symlink_in_path(subpath, allowed_write_paths) {
|
||||
// bwrap takes `--ro-bind <source> <destination>`. Use the resolved target
|
||||
// for both operands so a protected symlinked directory is remounted
|
||||
// read-only in place instead of binding onto the symlink path itself.
|
||||
let mount_source = path_to_string(&target);
|
||||
let mount_destination = path_to_string(&target);
|
||||
args.push("--ro-bind".to_string());
|
||||
args.push("/dev/null".to_string());
|
||||
args.push(path_to_string(&symlink_path));
|
||||
args.push(mount_source);
|
||||
args.push(mount_destination);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -458,11 +535,16 @@ fn append_unreadable_root_args(
|
||||
unreadable_root: &Path,
|
||||
allowed_write_paths: &[PathBuf],
|
||||
) -> Result<()> {
|
||||
if let Some(symlink_path) = find_symlink_in_path(unreadable_root, allowed_write_paths) {
|
||||
args.push("--ro-bind".to_string());
|
||||
args.push("/dev/null".to_string());
|
||||
args.push(path_to_string(&symlink_path));
|
||||
return Ok(());
|
||||
if let Some(target) = canonical_target_for_symlink_in_path(unreadable_root, allowed_write_paths)
|
||||
{
|
||||
// Apply unreadable handling to the resolved symlink target, not the
|
||||
// logical symlink path, to avoid file-vs-directory bind mismatches.
|
||||
return append_existing_unreadable_path_args(
|
||||
args,
|
||||
preserved_files,
|
||||
&target,
|
||||
allowed_write_paths,
|
||||
);
|
||||
}
|
||||
|
||||
if !unreadable_root.exists() {
|
||||
@@ -476,6 +558,20 @@ fn append_unreadable_root_args(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
append_existing_unreadable_path_args(
|
||||
args,
|
||||
preserved_files,
|
||||
unreadable_root,
|
||||
allowed_write_paths,
|
||||
)
|
||||
}
|
||||
|
||||
fn append_existing_unreadable_path_args(
|
||||
args: &mut Vec<String>,
|
||||
preserved_files: &mut Vec<File>,
|
||||
unreadable_root: &Path,
|
||||
allowed_write_paths: &[PathBuf],
|
||||
) -> Result<()> {
|
||||
if unreadable_root.is_dir() {
|
||||
let mut writable_descendants: Vec<&Path> = allowed_write_paths
|
||||
.iter()
|
||||
@@ -525,12 +621,10 @@ fn is_within_allowed_write_paths(path: &Path, allowed_write_paths: &[PathBuf]) -
|
||||
.any(|root| path.starts_with(root))
|
||||
}
|
||||
|
||||
/// Find the first symlink along `target_path` that is also under a writable root.
|
||||
///
|
||||
/// This blocks symlink replacement attacks where a protected path is a symlink
|
||||
/// inside a writable root (e.g., `.codex -> ./decoy`). In that case we mount
|
||||
/// `/dev/null` on the symlink itself to prevent rewiring it.
|
||||
fn find_symlink_in_path(target_path: &Path, allowed_write_paths: &[PathBuf]) -> Option<PathBuf> {
|
||||
fn canonical_target_for_symlink_in_path(
|
||||
target_path: &Path,
|
||||
allowed_write_paths: &[PathBuf],
|
||||
) -> Option<PathBuf> {
|
||||
let mut current = PathBuf::new();
|
||||
|
||||
for component in target_path.components() {
|
||||
@@ -557,7 +651,7 @@ fn find_symlink_in_path(target_path: &Path, allowed_write_paths: &[PathBuf]) ->
|
||||
if metadata.file_type().is_symlink()
|
||||
&& is_within_allowed_write_paths(¤t, allowed_write_paths)
|
||||
{
|
||||
return Some(current);
|
||||
return fs::canonicalize(¤t).ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -701,7 +795,13 @@ mod tests {
|
||||
BwrapOptions::default(),
|
||||
)
|
||||
.expect("create bwrap args");
|
||||
let canonical_sandbox_cwd = path_to_string(
|
||||
&real_root
|
||||
.canonicalize()
|
||||
.expect("canonicalize sandbox policy cwd"),
|
||||
);
|
||||
let canonical_command_cwd = path_to_string(&canonical_command_cwd);
|
||||
let link_sandbox_cwd = path_to_string(&link_root);
|
||||
let link_command_cwd = path_to_string(&command_cwd);
|
||||
|
||||
assert!(
|
||||
@@ -709,12 +809,199 @@ mod tests {
|
||||
.windows(2)
|
||||
.any(|window| { window == ["--chdir", canonical_command_cwd.as_str()] })
|
||||
);
|
||||
assert!(args.args.windows(3).any(|window| {
|
||||
window
|
||||
== [
|
||||
"--ro-bind",
|
||||
canonical_sandbox_cwd.as_str(),
|
||||
canonical_sandbox_cwd.as_str(),
|
||||
]
|
||||
}));
|
||||
assert!(
|
||||
!args
|
||||
.args
|
||||
.windows(2)
|
||||
.any(|window| { window == ["--chdir", link_command_cwd.as_str()] })
|
||||
);
|
||||
assert!(!args.args.windows(3).any(|window| {
|
||||
window
|
||||
== [
|
||||
"--ro-bind",
|
||||
link_sandbox_cwd.as_str(),
|
||||
link_sandbox_cwd.as_str(),
|
||||
]
|
||||
}));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn symlinked_writable_roots_bind_real_target_and_remap_carveouts() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let real_root = temp_dir.path().join("real");
|
||||
let link_root = temp_dir.path().join("link");
|
||||
let blocked = real_root.join("blocked");
|
||||
std::fs::create_dir_all(&blocked).expect("create blocked dir");
|
||||
std::os::unix::fs::symlink(&real_root, &link_root).expect("create symlinked root");
|
||||
|
||||
let link_root =
|
||||
AbsolutePathBuf::from_absolute_path(&link_root).expect("absolute symlinked root");
|
||||
let link_blocked = link_root.join("blocked");
|
||||
let real_root_str = path_to_string(&real_root);
|
||||
let real_blocked_str = path_to_string(&blocked);
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path: link_root },
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path: link_blocked },
|
||||
access: FileSystemAccessMode::None,
|
||||
},
|
||||
]);
|
||||
|
||||
let args = create_filesystem_args(&policy, temp_dir.path()).expect("filesystem args");
|
||||
|
||||
assert!(args.args.windows(3).any(|window| {
|
||||
window == ["--bind", real_root_str.as_str(), real_root_str.as_str()]
|
||||
}));
|
||||
assert!(args.args.windows(6).any(|window| {
|
||||
window
|
||||
== [
|
||||
"--perms",
|
||||
"000",
|
||||
"--tmpfs",
|
||||
real_blocked_str.as_str(),
|
||||
"--remount-ro",
|
||||
real_blocked_str.as_str(),
|
||||
]
|
||||
}));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn writable_roots_under_symlinked_ancestors_bind_real_target() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let logical_home = temp_dir.path().join("home");
|
||||
let real_codex = temp_dir.path().join("real-codex");
|
||||
let logical_codex = logical_home.join(".codex");
|
||||
let real_memories = real_codex.join("memories");
|
||||
let logical_memories = logical_codex.join("memories");
|
||||
std::fs::create_dir_all(&logical_home).expect("create logical home");
|
||||
std::fs::create_dir_all(&real_memories).expect("create memories dir");
|
||||
std::os::unix::fs::symlink(&real_codex, &logical_codex)
|
||||
.expect("create symlinked codex home");
|
||||
|
||||
let logical_memories_root =
|
||||
AbsolutePathBuf::from_absolute_path(&logical_memories).expect("absolute memories");
|
||||
let real_memories_str = path_to_string(&real_memories);
|
||||
let logical_memories_str = path_to_string(&logical_memories);
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: logical_memories_root,
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
}]);
|
||||
|
||||
let args = create_filesystem_args(&policy, temp_dir.path()).expect("filesystem args");
|
||||
|
||||
assert!(args.args.windows(3).any(|window| {
|
||||
window
|
||||
== [
|
||||
"--bind",
|
||||
real_memories_str.as_str(),
|
||||
real_memories_str.as_str(),
|
||||
]
|
||||
}));
|
||||
assert!(!args.args.windows(3).any(|window| {
|
||||
window
|
||||
== [
|
||||
"--bind",
|
||||
logical_memories_str.as_str(),
|
||||
logical_memories_str.as_str(),
|
||||
]
|
||||
}));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn protected_symlinked_directory_subpaths_bind_target_read_only() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let root = temp_dir.path().join("root");
|
||||
let agents_target = root.join("agents-target");
|
||||
let agents_link = root.join(".agents");
|
||||
std::fs::create_dir_all(&agents_target).expect("create agents target");
|
||||
std::os::unix::fs::symlink(&agents_target, &agents_link).expect("create symlinked .agents");
|
||||
|
||||
let root = AbsolutePathBuf::from_absolute_path(&root).expect("absolute root");
|
||||
let agents_link_str = path_to_string(&agents_link);
|
||||
let agents_target_str = path_to_string(&agents_target);
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path: root },
|
||||
access: FileSystemAccessMode::Write,
|
||||
}]);
|
||||
|
||||
let args = create_filesystem_args(&policy, temp_dir.path()).expect("filesystem args");
|
||||
|
||||
assert!(args.args.windows(3).any(|window| {
|
||||
window
|
||||
== [
|
||||
"--ro-bind",
|
||||
agents_target_str.as_str(),
|
||||
agents_target_str.as_str(),
|
||||
]
|
||||
}));
|
||||
assert!(!args.args.iter().any(|arg| arg == agents_link_str.as_str()));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn symlinked_writable_roots_mask_nested_symlink_escape_paths_without_binding_targets() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let real_root = temp_dir.path().join("real");
|
||||
let link_root = temp_dir.path().join("link");
|
||||
let outside = temp_dir.path().join("outside-private");
|
||||
let linked_private = real_root.join("linked-private");
|
||||
std::fs::create_dir_all(&real_root).expect("create real root");
|
||||
std::fs::create_dir_all(&outside).expect("create outside dir");
|
||||
std::os::unix::fs::symlink(&real_root, &link_root).expect("create symlinked root");
|
||||
std::os::unix::fs::symlink(&outside, &linked_private)
|
||||
.expect("create nested escape symlink");
|
||||
|
||||
let link_root =
|
||||
AbsolutePathBuf::from_absolute_path(&link_root).expect("absolute symlinked root");
|
||||
let link_private = link_root.join("linked-private");
|
||||
let real_linked_private_str = path_to_string(&linked_private);
|
||||
let outside_str = path_to_string(&outside);
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path: link_root },
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path: link_private },
|
||||
access: FileSystemAccessMode::None,
|
||||
},
|
||||
]);
|
||||
|
||||
let args = create_filesystem_args(&policy, temp_dir.path()).expect("filesystem args");
|
||||
|
||||
assert!(args.args.windows(6).any(|window| {
|
||||
window
|
||||
== [
|
||||
"--perms",
|
||||
"000",
|
||||
"--tmpfs",
|
||||
outside_str.as_str(),
|
||||
"--remount-ro",
|
||||
outside_str.as_str(),
|
||||
]
|
||||
}));
|
||||
assert!(
|
||||
!args
|
||||
.args
|
||||
.iter()
|
||||
.any(|arg| arg == real_linked_private_str.as_str())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -128,7 +128,6 @@ async fn pro_account_with_no_api_key_uses_chatgpt_auth() {
|
||||
chatgpt_plan_type: Some(InternalPlanType::Known(InternalKnownPlan::Pro)),
|
||||
chatgpt_user_id: Some("user-12345".to_string()),
|
||||
chatgpt_account_id: None,
|
||||
is_org_owner: None,
|
||||
raw_jwt: fake_jwt,
|
||||
},
|
||||
access_token: "test-access-token".to_string(),
|
||||
|
||||
@@ -302,14 +302,6 @@ impl CodexAuth {
|
||||
.and_then(|t| t.id_token.chatgpt_user_id)
|
||||
}
|
||||
|
||||
/// Returns `Some(true)` when the current ChatGPT token marks the user as a
|
||||
/// workspace owner, `Some(false)` for non-owners, and `None` when auth is
|
||||
/// unavailable or the token omits the claim.
|
||||
pub fn is_workspace_owner(&self) -> Option<bool> {
|
||||
self.get_current_token_data()
|
||||
.and_then(|t| t.id_token.is_org_owner)
|
||||
}
|
||||
|
||||
/// Account-facing plan classification derived from the current token.
|
||||
/// Returns a high-level `AccountPlanType` (e.g., Free/Plus/Pro/Team/…)
|
||||
/// mapped from the ID token's internal plan value. Prefer this when you
|
||||
|
||||
@@ -36,8 +36,6 @@ pub struct IdTokenInfo {
|
||||
pub chatgpt_user_id: Option<String>,
|
||||
/// Organization/workspace identifier associated with the token, if present.
|
||||
pub chatgpt_account_id: Option<String>,
|
||||
/// Whether the current user owns the workspace, when present in the token.
|
||||
pub is_org_owner: Option<bool>,
|
||||
pub raw_jwt: String,
|
||||
}
|
||||
|
||||
@@ -90,8 +88,6 @@ struct AuthClaims {
|
||||
user_id: Option<String>,
|
||||
#[serde(default)]
|
||||
chatgpt_account_id: Option<String>,
|
||||
#[serde(default)]
|
||||
is_org_owner: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -143,7 +139,6 @@ pub fn parse_chatgpt_jwt_claims(jwt: &str) -> Result<IdTokenInfo, IdTokenInfoErr
|
||||
chatgpt_plan_type: auth.chatgpt_plan_type,
|
||||
chatgpt_user_id: auth.chatgpt_user_id.or(auth.user_id),
|
||||
chatgpt_account_id: auth.chatgpt_account_id,
|
||||
is_org_owner: auth.is_org_owner,
|
||||
}),
|
||||
None => Ok(IdTokenInfo {
|
||||
email,
|
||||
@@ -151,7 +146,6 @@ pub fn parse_chatgpt_jwt_claims(jwt: &str) -> Result<IdTokenInfo, IdTokenInfoErr
|
||||
chatgpt_plan_type: None,
|
||||
chatgpt_user_id: None,
|
||||
chatgpt_account_id: None,
|
||||
is_org_owner: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,21 +107,6 @@ fn id_token_info_parses_usage_based_business_plans() {
|
||||
assert_eq!(enterprise_cbp.is_workspace_account(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn id_token_info_parses_workspace_owner_flag() {
|
||||
let fake_jwt = fake_jwt(serde_json::json!({
|
||||
"email": "owner@example.com",
|
||||
"https://api.openai.com/auth": {
|
||||
"chatgpt_plan_type": "self_serve_business_usage_based",
|
||||
"is_org_owner": true
|
||||
}
|
||||
}));
|
||||
|
||||
let info = parse_chatgpt_jwt_claims(&fake_jwt).expect("should parse");
|
||||
assert_eq!(info.email.as_deref(), Some("owner@example.com"));
|
||||
assert_eq!(info.is_org_owner, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn id_token_info_handles_missing_fields() {
|
||||
let fake_jwt = fake_jwt(serde_json::json!({ "sub": "123" }));
|
||||
|
||||
@@ -339,7 +339,6 @@ async fn run_codex_tool_session_inner(
|
||||
| EventMsg::McpToolCallEnd(_)
|
||||
| EventMsg::McpListToolsResponse(_)
|
||||
| EventMsg::ListSkillsResponse(_)
|
||||
| EventMsg::AddCreditsNudgeEmailResponse(_)
|
||||
| EventMsg::RealtimeConversationListVoicesResponse(_)
|
||||
| EventMsg::ExecCommandBegin(_)
|
||||
| EventMsg::TerminalInteraction(_)
|
||||
|
||||
@@ -108,6 +108,12 @@ pub enum GuardianAssessmentStatus {
|
||||
Aborted,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GuardianAssessmentDecisionSource {
|
||||
Agent,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GuardianCommandSource {
|
||||
@@ -159,6 +165,10 @@ pub struct NetworkPolicyAmendment {
|
||||
pub struct GuardianAssessmentEvent {
|
||||
/// Stable identifier for this guardian review lifecycle.
|
||||
pub id: String,
|
||||
/// Thread item being reviewed, when the review maps to a concrete item.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub target_item_id: Option<String>,
|
||||
/// Turn ID that this assessment belongs to.
|
||||
/// Uses `#[serde(default)]` for backwards compatibility.
|
||||
#[serde(default)]
|
||||
@@ -176,6 +186,10 @@ pub struct GuardianAssessmentEvent {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub rationale: Option<String>,
|
||||
/// Source that produced the terminal assessment decision.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub decision_source: Option<GuardianAssessmentDecisionSource>,
|
||||
/// Canonical action payload that was reviewed.
|
||||
pub action: GuardianAssessmentAction,
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ fn rate_limit_snapshot() -> RateLimitSnapshot {
|
||||
resets_at: Some(secondary_reset_at),
|
||||
}),
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_absolute_path::canonicalize_preserving_symlinks;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -471,6 +472,9 @@ impl FileSystemSandboxPolicy {
|
||||
// so root-wide aliases do not create duplicate top-level masks.
|
||||
// Example: keep `/var/...` normalized under `/` instead of
|
||||
// materializing both `/var/...` and `/private/var/...`.
|
||||
// Nested symlink paths under a writable root stay logical so
|
||||
// downstream sandboxes can still bind the real target while
|
||||
// masking the user-visible symlink inode when needed.
|
||||
let preserve_raw_carveout_paths = root.as_path().parent().is_some();
|
||||
let raw_writable_roots: Vec<&AbsolutePathBuf> = writable_entries
|
||||
.iter()
|
||||
@@ -1081,14 +1085,17 @@ fn dedup_absolute_paths(
|
||||
fn normalize_effective_absolute_path(path: AbsolutePathBuf) -> AbsolutePathBuf {
|
||||
let raw_path = path.to_path_buf();
|
||||
for ancestor in raw_path.ancestors() {
|
||||
let Ok(canonical_ancestor) = ancestor.canonicalize() else {
|
||||
if std::fs::symlink_metadata(ancestor).is_err() {
|
||||
continue;
|
||||
}
|
||||
let Ok(normalized_ancestor) = canonicalize_preserving_symlinks(ancestor) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(suffix) = raw_path.strip_prefix(ancestor) else {
|
||||
continue;
|
||||
};
|
||||
if let Ok(normalized_path) =
|
||||
AbsolutePathBuf::from_absolute_path(canonical_ancestor.join(suffix))
|
||||
AbsolutePathBuf::from_absolute_path(normalized_ancestor.join(suffix))
|
||||
{
|
||||
return normalized_path;
|
||||
}
|
||||
@@ -1422,7 +1429,7 @@ mod tests {
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn effective_runtime_roots_canonicalize_symlinked_paths() {
|
||||
fn effective_runtime_roots_preserve_symlinked_paths() {
|
||||
let cwd = TempDir::new().expect("tempdir");
|
||||
let real_root = cwd.path().join("real");
|
||||
let link_root = cwd.path().join("link");
|
||||
@@ -1436,18 +1443,9 @@ mod tests {
|
||||
let link_root =
|
||||
AbsolutePathBuf::from_absolute_path(&link_root).expect("absolute symlinked root");
|
||||
let link_blocked = link_root.join("blocked");
|
||||
let expected_root = AbsolutePathBuf::from_absolute_path(
|
||||
real_root.canonicalize().expect("canonicalize real root"),
|
||||
)
|
||||
.expect("absolute canonical root");
|
||||
let expected_blocked = AbsolutePathBuf::from_absolute_path(
|
||||
blocked.canonicalize().expect("canonicalize blocked"),
|
||||
)
|
||||
.expect("absolute canonical blocked");
|
||||
let expected_codex = AbsolutePathBuf::from_absolute_path(
|
||||
codex_dir.canonicalize().expect("canonicalize .codex"),
|
||||
)
|
||||
.expect("absolute canonical .codex");
|
||||
let expected_root = link_root.clone();
|
||||
let expected_blocked = link_blocked.clone();
|
||||
let expected_codex = link_root.join(".codex");
|
||||
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
@@ -1482,7 +1480,7 @@ mod tests {
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn current_working_directory_special_path_canonicalizes_symlinked_cwd() {
|
||||
fn current_working_directory_special_path_preserves_symlinked_cwd() {
|
||||
let cwd = TempDir::new().expect("tempdir");
|
||||
let real_root = cwd.path().join("real");
|
||||
let link_root = cwd.path().join("link");
|
||||
@@ -1497,22 +1495,11 @@ mod tests {
|
||||
|
||||
let link_blocked =
|
||||
AbsolutePathBuf::from_absolute_path(link_root.join("blocked")).expect("link blocked");
|
||||
let expected_root = AbsolutePathBuf::from_absolute_path(
|
||||
real_root.canonicalize().expect("canonicalize real root"),
|
||||
)
|
||||
.expect("absolute canonical root");
|
||||
let expected_blocked = AbsolutePathBuf::from_absolute_path(
|
||||
blocked.canonicalize().expect("canonicalize blocked"),
|
||||
)
|
||||
.expect("absolute canonical blocked");
|
||||
let expected_agents = AbsolutePathBuf::from_absolute_path(
|
||||
agents_dir.canonicalize().expect("canonicalize .agents"),
|
||||
)
|
||||
.expect("absolute canonical .agents");
|
||||
let expected_codex = AbsolutePathBuf::from_absolute_path(
|
||||
codex_dir.canonicalize().expect("canonicalize .codex"),
|
||||
)
|
||||
.expect("absolute canonical .codex");
|
||||
let expected_root =
|
||||
AbsolutePathBuf::from_absolute_path(&link_root).expect("absolute symlinked root");
|
||||
let expected_blocked = link_blocked.clone();
|
||||
let expected_agents = expected_root.join(".agents");
|
||||
let expected_codex = expected_root.join(".codex");
|
||||
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
@@ -1617,11 +1604,8 @@ mod tests {
|
||||
let link_root =
|
||||
AbsolutePathBuf::from_absolute_path(&link_root).expect("absolute symlinked root");
|
||||
let link_private = link_root.join("linked-private");
|
||||
let expected_root = AbsolutePathBuf::from_absolute_path(
|
||||
real_root.canonicalize().expect("canonicalize real root"),
|
||||
)
|
||||
.expect("absolute canonical root");
|
||||
let expected_linked_private = expected_root.join("linked-private");
|
||||
let expected_root = link_root.clone();
|
||||
let expected_linked_private = link_private.clone();
|
||||
let unexpected_decoy =
|
||||
AbsolutePathBuf::from_absolute_path(decoy.canonicalize().expect("canonicalize decoy"))
|
||||
.expect("absolute canonical decoy");
|
||||
@@ -1667,11 +1651,8 @@ mod tests {
|
||||
let link_root =
|
||||
AbsolutePathBuf::from_absolute_path(&link_root).expect("absolute symlinked root");
|
||||
let link_private = link_root.join("linked-private");
|
||||
let expected_root = AbsolutePathBuf::from_absolute_path(
|
||||
real_root.canonicalize().expect("canonicalize real root"),
|
||||
)
|
||||
.expect("absolute canonical root");
|
||||
let expected_linked_private = expected_root.join("linked-private");
|
||||
let expected_root = link_root.clone();
|
||||
let expected_linked_private = link_private.clone();
|
||||
let unexpected_decoy =
|
||||
AbsolutePathBuf::from_absolute_path(decoy.canonicalize().expect("canonicalize decoy"))
|
||||
.expect("absolute canonical decoy");
|
||||
@@ -1737,12 +1718,12 @@ mod tests {
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn tmpdir_special_path_canonicalizes_symlinked_tmpdir() {
|
||||
fn tmpdir_special_path_preserves_symlinked_tmpdir() {
|
||||
if std::env::var_os(SYMLINKED_TMPDIR_TEST_ENV).is_none() {
|
||||
let output = std::process::Command::new(std::env::current_exe().expect("test binary"))
|
||||
.env(SYMLINKED_TMPDIR_TEST_ENV, "1")
|
||||
.arg("--exact")
|
||||
.arg("permissions::tests::tmpdir_special_path_canonicalizes_symlinked_tmpdir")
|
||||
.arg("permissions::tests::tmpdir_special_path_preserves_symlinked_tmpdir")
|
||||
.output()
|
||||
.expect("run tmpdir subprocess test");
|
||||
|
||||
@@ -1767,20 +1748,10 @@ mod tests {
|
||||
|
||||
let link_blocked =
|
||||
AbsolutePathBuf::from_absolute_path(link_tmpdir.join("blocked")).expect("link blocked");
|
||||
let expected_root = AbsolutePathBuf::from_absolute_path(
|
||||
real_tmpdir
|
||||
.canonicalize()
|
||||
.expect("canonicalize real tmpdir"),
|
||||
)
|
||||
.expect("absolute canonical tmpdir");
|
||||
let expected_blocked = AbsolutePathBuf::from_absolute_path(
|
||||
blocked.canonicalize().expect("canonicalize blocked"),
|
||||
)
|
||||
.expect("absolute canonical blocked");
|
||||
let expected_codex = AbsolutePathBuf::from_absolute_path(
|
||||
codex_dir.canonicalize().expect("canonicalize .codex"),
|
||||
)
|
||||
.expect("absolute canonical .codex");
|
||||
let expected_root =
|
||||
AbsolutePathBuf::from_absolute_path(&link_tmpdir).expect("absolute symlinked tmpdir");
|
||||
let expected_blocked = link_blocked.clone();
|
||||
let expected_codex = expected_root.join(".codex");
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("TMPDIR", &link_tmpdir);
|
||||
@@ -1932,8 +1903,7 @@ mod tests {
|
||||
let cwd = TempDir::new().expect("tempdir");
|
||||
let docs = AbsolutePathBuf::resolve_path_against_base("docs", cwd.path());
|
||||
let expected_docs = AbsolutePathBuf::from_absolute_path(
|
||||
cwd.path()
|
||||
.canonicalize()
|
||||
canonicalize_preserving_symlinks(cwd.path())
|
||||
.expect("canonicalize cwd")
|
||||
.join("docs"),
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user