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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
428 changes: 373 additions & 55 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions crates/webcodex-agent-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,9 @@ pub fn generated_agent_config_toml(opts: &AgentInitOptions) -> Result<String, St
// be inferred from computer_control in generated static config.
computer_text_input: false,
job_state_reconciliation: false,
// ACP autonomous coding is a runtime-only capability and must not be
// silently enabled by generated legacy agent config.
coding_agent_runs: false,
},
policy: GeneratedAgentPolicy {
allow_raw_shell: true,
Expand Down
9 changes: 9 additions & 0 deletions crates/webcodex-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ fn parse_connect(args: &[String]) -> CliAction {
let mut oauth_redirect_uri = None;
let mut oauth_computer_permissions = false;
let mut oauth_local_mcp = false;
let mut oauth_coding_agent = false;
let mut username = None;
let mut project = PathBuf::from(".");
let mut profile = None;
Expand Down Expand Up @@ -424,6 +425,7 @@ fn parse_connect(args: &[String]) -> CliAction {
},
"--oauth-computer-permissions" => oauth_computer_permissions = true,
"--oauth-local-mcp" => oauth_local_mcp = true,
"--oauth-coding-agent" => oauth_coding_agent = true,
"--user" | "--username" => match take(&mut index) {
Some(value) => username = Some(value),
None => return cli_parse_error(format!("{arg} requires a value")),
Expand Down Expand Up @@ -495,6 +497,9 @@ fn parse_connect(args: &[String]) -> CliAction {
if oauth_local_mcp {
return cli_parse_error("--oauth-local-mcp requires --auth oauth".to_string());
}
if oauth_coding_agent {
return cli_parse_error("--oauth-coding-agent requires --auth oauth".to_string());
}
if key.is_some() || key_file.is_some() {
return cli_parse_error(
"--auth managed-oauth cannot be combined with --key or --key-file".to_string(),
Expand All @@ -518,6 +523,9 @@ fn parse_connect(args: &[String]) -> CliAction {
if oauth_local_mcp {
return cli_parse_error("--oauth-local-mcp requires --auth oauth".to_string());
}
if oauth_coding_agent {
return cli_parse_error("--oauth-coding-agent requires --auth oauth".to_string());
}
if oauth_redirect_uri.is_some() || username.is_some() {
return cli_parse_error(
"--oauth-redirect-uri requires --auth oauth or managed-oauth; --user requires --auth managed-oauth"
Expand Down Expand Up @@ -546,6 +554,7 @@ fn parse_connect(args: &[String]) -> CliAction {
oauth_redirect_uri,
oauth_computer_permissions,
oauth_local_mcp,
oauth_coding_agent,
username,
project,
profile,
Expand Down
1 change: 1 addition & 0 deletions crates/webcodex-cli/src/webcodex_cli/connect/oauth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -951,6 +951,7 @@ mod tests {
oauth_redirect_uri: Some("https://client.example/callback".to_string()),
oauth_computer_permissions: false,
oauth_local_mcp: false,
oauth_coding_agent: false,
username: None,
project: PathBuf::from("."),
profile: None,
Expand Down
2 changes: 2 additions & 0 deletions crates/webcodex-cli/src/webcodex_cli/connect/profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub(crate) struct ConnectOptions {
pub(crate) oauth_redirect_uri: Option<String>,
pub(crate) oauth_computer_permissions: bool,
pub(crate) oauth_local_mcp: bool,
pub(crate) oauth_coding_agent: bool,
pub(crate) username: Option<String>,
pub(crate) project: PathBuf,
pub(crate) profile: Option<String>,
Expand Down Expand Up @@ -773,6 +774,7 @@ mod tests {
oauth_redirect_uri: None,
oauth_computer_permissions: false,
oauth_local_mcp: false,
oauth_coding_agent: false,
username: None,
project: project.clone(),
profile: None,
Expand Down
44 changes: 38 additions & 6 deletions crates/webcodex-cli/src/webcodex_cli/connect/shared_key_oauth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const BRIDGE_PROFILE_VERSION: u32 = 1;
const BRIDGE_PROFILE_PREFIX: &str = "shared-key-oauth-";
const BRIDGE_SECRET_DISCLOSED_PREFIX: &str = ".shared-key-oauth-secret-disclosed-";
const LOCAL_MCP_SCOPE: &str = "mcp:local";
const CODING_AGENT_SCOPE: &str = "coding_agent:run";
const BRIDGE_BASELINE_SCOPES: &[&str] = &[
"runtime:read",
"project:read",
Expand Down Expand Up @@ -55,6 +56,8 @@ struct SharedKeyOAuthProfile {
computer_permissions_enabled: bool,
#[serde(default)]
local_mcp_enabled: bool,
#[serde(default)]
coding_agent_enabled: bool,
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -127,10 +130,10 @@ fn scope_list_is_unique(scopes: &[String]) -> bool {
== scopes.len()
}

fn without_local_mcp(scopes: &[String]) -> Vec<String> {
fn without_optional_class_scopes(scopes: &[String]) -> Vec<String> {
scopes
.iter()
.filter(|scope| scope.as_str() != LOCAL_MCP_SCOPE)
.filter(|scope| !matches!(scope.as_str(), LOCAL_MCP_SCOPE | CODING_AGENT_SCOPE))
.cloned()
.collect()
}
Expand Down Expand Up @@ -188,7 +191,14 @@ fn profile_scope_ceiling_is_valid(profile: &SharedKeyOAuthProfile) -> bool {
if local_mcp_present != profile.local_mcp_enabled {
return false;
}
let authority_scopes = without_local_mcp(&profile.allowed_scopes);
let coding_agent_present = profile
.allowed_scopes
.iter()
.any(|scope| scope == CODING_AGENT_SCOPE);
if coding_agent_present != profile.coding_agent_enabled {
return false;
}
let authority_scopes = without_optional_class_scopes(&profile.allowed_scopes);
if profile.computer_permissions_enabled {
computer_enabled_scope_ceiling_is_valid(&authority_scopes)
} else {
Expand Down Expand Up @@ -282,6 +292,7 @@ async fn provision_client(
"previous_allowed_scopes": existing.map(|profile| profile.allowed_scopes.as_slice()),
"computer_permissions": opts.oauth_computer_permissions,
"local_mcp": opts.oauth_local_mcp,
"coding_agent": opts.oauth_coding_agent,
}),
})
.await?;
Expand Down Expand Up @@ -315,15 +326,24 @@ async fn provision_client(
.to_string(),
);
}
let authority_scopes = without_local_mcp(&allowed_scopes);
let coding_agent_present = allowed_scopes
.iter()
.any(|scope| scope == CODING_AGENT_SCOPE);
if coding_agent_present != opts.oauth_coding_agent {
return Err(
"Server changed coding-agent OAuth authority without matching the explicit connect opt-in"
.to_string(),
);
}
let authority_scopes = without_optional_class_scopes(&allowed_scopes);
if opts.oauth_computer_permissions {
if !computer_enabled_scope_ceiling_is_valid(&authority_scopes) {
return Err(
"Server returned an invalid Computer-enabled shared-key OAuth ceiling".to_string(),
);
}
let expected_scopes = if let Some(existing) = existing {
computer_enabled_scope_ceiling_from_existing(&without_local_mcp(&existing.allowed_scopes))
computer_enabled_scope_ceiling_from_existing(&without_optional_class_scopes(&existing.allowed_scopes))
.ok_or_else(|| {
"existing shared-key OAuth profile cannot be safely upgraded to Computer permissions"
.to_string()
Expand All @@ -348,7 +368,7 @@ async fn provision_client(
);
}
let expected_scopes = existing
.map(|profile| without_local_mcp(&profile.allowed_scopes))
.map(|profile| without_optional_class_scopes(&profile.allowed_scopes))
.unwrap_or_else(|| {
BRIDGE_BASELINE_SCOPES
.iter()
Expand Down Expand Up @@ -381,6 +401,7 @@ async fn provision_client(
updated.allowed_scopes = allowed_scopes;
updated.computer_permissions_enabled = opts.oauth_computer_permissions;
updated.local_mcp_enabled = opts.oauth_local_mcp;
updated.coding_agent_enabled = opts.oauth_coding_agent;
let changed = updated != *existing;
return Ok((updated, changed));
}
Expand All @@ -400,6 +421,7 @@ async fn provision_client(
allowed_scopes,
computer_permissions_enabled: opts.oauth_computer_permissions,
local_mcp_enabled: opts.oauth_local_mcp,
coding_agent_enabled: opts.oauth_coding_agent,
},
true,
))
Expand Down Expand Up @@ -488,6 +510,12 @@ pub(super) async fn finish_shared_key_oauth_connect(
.to_string(),
);
}
if existing.coding_agent_enabled && !opts.oauth_coding_agent {
return Err(
"this shared-key OAuth profile already has coding-agent authority enabled; reconnect with --oauth-coding-agent to reuse it, or use a different profile/redirect URI"
.to_string(),
);
}
}
let metadata = fetch_metadata(opts, server_url).await?;
let (oauth, created_or_rotated) = provision_client(
Expand Down Expand Up @@ -554,6 +582,7 @@ mod tests {
oauth_redirect_uri: Some("https://chatgpt.example/callback".to_string()),
oauth_computer_permissions: false,
oauth_local_mcp: false,
oauth_coding_agent: false,
username: None,
project: PathBuf::from("."),
profile: None,
Expand Down Expand Up @@ -689,6 +718,7 @@ mod tests {
allowed_scopes: vec!["runtime:read".to_string(), "project:read".to_string()],
computer_permissions_enabled: false,
local_mcp_enabled: false,
coding_agent_enabled: false,
};
let (upgraded, changed) = provision_client(
&opts,
Expand Down Expand Up @@ -820,6 +850,7 @@ mod tests {
allowed_scopes: vec!["runtime:read".to_string(), "project:read".to_string()],
computer_permissions_enabled: false,
local_mcp_enabled: false,
coding_agent_enabled: false,
};
assert!(profile_scope_ceiling_is_valid(&baseline));

Expand Down Expand Up @@ -905,6 +936,7 @@ mod tests {
allowed_scopes: vec!["runtime:read".to_string()],
computer_permissions_enabled: false,
local_mcp_enabled: false,
coding_agent_enabled: false,
};
let state_path = Path::new("/protected/profile/shared-key-oauth.toml");
let first = bridge_client_secret_line(&oauth, true, state_path);
Expand Down
1 change: 1 addition & 0 deletions crates/webcodex-cli/src/webcodex_cli/tests/connect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ async fn connect_rejects_invalid_url_and_missing_project_before_network_or_write
oauth_redirect_uri: None,
oauth_computer_permissions: false,
oauth_local_mcp: false,
oauth_coding_agent: false,
username: None,
project: tmp.path().join("missing"),
profile: None,
Expand Down
4 changes: 3 additions & 1 deletion crates/webcodex-cli/src/webcodex_cli/usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Options:\n\
--oauth-computer-permissions\n\
Allow ordinary OAuth browser consent to offer optional Computer permissions\n\
--oauth-local-mcp Explicitly allow this OAuth client to request mcp:local authority\n\
--oauth-coding-agent Explicitly allow this OAuth client to request coding_agent:run authority\n\
--user USER Select a logged-in managed user; managed-oauth only\n\
--key KEY Shared key (use --key-file to avoid shell history)\n\
--key-file PATH Read the shared key from a file\n\
Expand All @@ -68,7 +69,8 @@ browser authorize page; ChatGPT receives OAuth client credentials/tokens, never
Without explicit opt-ins the bridge keeps the direct shared-key model-facing baseline.\n\
--oauth-computer-permissions adds only the fixed launch/display/pointer/clipboard Computer\n\
ceiling; browser checkboxes decide the actual grant. --oauth-local-mcp adds class-level\n\
mcp:local authority for Runner-owned MCP providers in this shared-key group. Existing\n\
mcp:local authority for Runner-owned MCP providers in this shared-key group.\n\
--oauth-coding-agent adds only coding_agent:run delegated coding-agent authority. Existing\n\
clients are never widened implicitly. managed-oauth remains a separate managed-user flow.\n"
}

Expand Down
Loading
Loading