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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 51 additions & 9 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,7 +12,8 @@ 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 LOCAL_PLUGIN_SCOPE: &str = "plugin:local";
const LOCAL_PLUGIN_INSPECT_SCOPE: &str = "plugin:inspect";
const LOCAL_PLUGIN_INVOKE_SCOPE: &str = "plugin:invoke";
const LOCAL_SSH_SCOPE: &str = "ssh:local";
const CODING_AGENT_SCOPE: &str = "coding_agent:run";
const BRIDGE_BASELINE_SCOPES: &[&str] = &[
Expand Down Expand Up @@ -144,7 +145,11 @@ fn without_optional_class_scopes(scopes: &[String]) -> Vec<String> {
.filter(|scope| {
!matches!(
scope.as_str(),
LOCAL_MCP_SCOPE | LOCAL_PLUGIN_SCOPE | LOCAL_SSH_SCOPE | CODING_AGENT_SCOPE
LOCAL_MCP_SCOPE
| LOCAL_PLUGIN_INSPECT_SCOPE
| LOCAL_PLUGIN_INVOKE_SCOPE
| LOCAL_SSH_SCOPE
| CODING_AGENT_SCOPE
)
})
.cloned()
Expand Down Expand Up @@ -204,11 +209,17 @@ fn profile_scope_ceiling_is_valid(profile: &SharedKeyOAuthProfile) -> bool {
if local_mcp_present != profile.local_mcp_enabled {
return false;
}
let local_plugins_present = profile
let local_plugin_inspect_present = profile
.allowed_scopes
.iter()
.any(|scope| scope == LOCAL_PLUGIN_SCOPE);
if local_plugins_present != profile.local_plugins_enabled {
.any(|scope| scope == LOCAL_PLUGIN_INSPECT_SCOPE);
let local_plugin_invoke_present = profile
.allowed_scopes
.iter()
.any(|scope| scope == LOCAL_PLUGIN_INVOKE_SCOPE);
if local_plugin_inspect_present != profile.local_plugins_enabled
|| local_plugin_invoke_present != profile.local_plugins_enabled
{
return false;
}
let local_ssh_present = profile
Expand Down Expand Up @@ -355,10 +366,15 @@ async fn provision_client(
.to_string(),
);
}
let local_plugins_present = allowed_scopes
let local_plugin_inspect_present = allowed_scopes
.iter()
.any(|scope| scope == LOCAL_PLUGIN_SCOPE);
if local_plugins_present != opts.oauth_local_plugins {
.any(|scope| scope == LOCAL_PLUGIN_INSPECT_SCOPE);
let local_plugin_invoke_present = allowed_scopes
.iter()
.any(|scope| scope == LOCAL_PLUGIN_INVOKE_SCOPE);
if local_plugin_inspect_present != opts.oauth_local_plugins
|| local_plugin_invoke_present != opts.oauth_local_plugins
{
return Err(
"Server changed local Plugin OAuth authority without matching the explicit connect opt-in"
.to_string(),
Expand Down Expand Up @@ -935,11 +951,37 @@ mod tests {
local_plugins.local_plugins_enabled = true;
local_plugins
.allowed_scopes
.push(LOCAL_PLUGIN_SCOPE.to_string());
.push(LOCAL_PLUGIN_INSPECT_SCOPE.to_string());
local_plugins
.allowed_scopes
.push(LOCAL_PLUGIN_INVOKE_SCOPE.to_string());
assert!(profile_scope_ceiling_is_valid(&local_plugins));
let mut mismatched_local_plugins = local_plugins.clone();
mismatched_local_plugins.local_plugins_enabled = false;
assert!(!profile_scope_ceiling_is_valid(&mismatched_local_plugins));
let mut inspect_only_plugins = baseline.clone();
inspect_only_plugins.local_plugins_enabled = true;
inspect_only_plugins
.allowed_scopes
.push(LOCAL_PLUGIN_INSPECT_SCOPE.to_string());
assert!(!profile_scope_ceiling_is_valid(&inspect_only_plugins));
let mut invoke_only_plugins = baseline.clone();
invoke_only_plugins.local_plugins_enabled = true;
invoke_only_plugins
.allowed_scopes
.push(LOCAL_PLUGIN_INVOKE_SCOPE.to_string());
assert!(!profile_scope_ceiling_is_valid(&invoke_only_plugins));
let mut legacy_plugin_scope = baseline.clone();
legacy_plugin_scope.local_plugins_enabled = true;
legacy_plugin_scope
.allowed_scopes
.push("plugin:local".to_string());
assert!(!profile_scope_ceiling_is_valid(&legacy_plugin_scope));
let mut manage_plugin_scope = local_plugins.clone();
manage_plugin_scope
.allowed_scopes
.push("plugin:manage".to_string());
assert!(!profile_scope_ceiling_is_valid(&manage_plugin_scope));

let mut local_ssh = baseline.clone();
local_ssh.local_ssh_enabled = true;
Expand Down
4 changes: 2 additions & 2 deletions crates/webcodex-cli/src/webcodex_cli/usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,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-local-plugins Explicitly allow this OAuth client to request plugin:local authority\n\
--oauth-local-plugins Explicitly allow this OAuth client to request plugin:inspect + plugin:invoke authority\n\
--oauth-local-ssh Explicitly allow this OAuth client to request ssh: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\
Expand All @@ -68,7 +68,7 @@ Without explicit opt-ins the bridge keeps the direct shared-key model-facing bas
--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.\n\
--oauth-local-plugins independently adds plugin:local authority for Runner-owned native Tool Plugins.\n\
--oauth-local-plugins independently adds plugin:inspect + plugin:invoke authority for Runner-owned native Tool Plugins; it never grants plugin:manage.\n\
--oauth-local-ssh independently adds ssh:local authority for Runner-local managed SSH resources.\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
8 changes: 6 additions & 2 deletions crates/webcodex-core/src/authority.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ pub const SCOPE_COMPUTER_POINTER_CONTROL: &str = "computer:pointer_control";
pub const SCOPE_COMPUTER_CLIPBOARD_READ: &str = "computer:clipboard_read";
pub const SCOPE_COMPUTER_CLIPBOARD_WRITE: &str = "computer:clipboard_write";
pub const SCOPE_MCP_LOCAL: &str = "mcp:local";
pub const SCOPE_PLUGIN_LOCAL: &str = "plugin:local";
pub const SCOPE_PLUGIN_INSPECT: &str = "plugin:inspect";
pub const SCOPE_PLUGIN_INVOKE: &str = "plugin:invoke";
pub const SCOPE_PLUGIN_MANAGE: &str = "plugin:manage";
pub const SCOPE_SSH_LOCAL: &str = "ssh:local";
pub const SCOPE_CODING_AGENT_RUN: &str = "coding_agent:run";
pub const SCOPE_AGENT_REGISTER: &str = "agent:register";
Expand Down Expand Up @@ -120,7 +122,9 @@ pub const KNOWN_SCOPES: &[&str] = &[
SCOPE_COMPUTER_LAUNCH,
SCOPE_COMPUTER_DISPLAY_READ,
SCOPE_MCP_LOCAL,
SCOPE_PLUGIN_LOCAL,
SCOPE_PLUGIN_INSPECT,
SCOPE_PLUGIN_INVOKE,
SCOPE_PLUGIN_MANAGE,
SCOPE_SSH_LOCAL,
SCOPE_CODING_AGENT_RUN,
SCOPE_ACCOUNT_MANAGE,
Expand Down
25 changes: 16 additions & 9 deletions docs/PLUGINS.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ provider contract violation and retires that provider instance fail-closed.

`plugin_tool call` requires an opaque binding from a preceding `describe`.
Bindings are bounded server-side observations, not bearer authorization tokens:
every call still requires current `plugin:local` authority and current access to
every call still requires current `plugin:invoke` authority and current access to
the logical Runner. A binding can also be evicted. If its Runner/provider
instance disappears, the tool is removed, or its schema changes, the stale call
fails `NotStarted` and must be described again. WebCodex never re-resolves the
Expand Down Expand Up @@ -337,16 +337,23 @@ registration catalog.

## OAuth

Native Plugin access is a separate authority: `plugin:local`.
Native Plugin authority is operation-specific:

- Without `plugin:local`, `plugin_tool` and first-class startup Plugin tools are
omitted from MCP `tools/list` and direct spoofed calls are rejected.
- `plugin:local` is not part of the shared-key OAuth baseline.
- `plugin:inspect` allows metadata observation such as list and describe.
- `plugin:invoke` allows `plugin_tool call` and first-class startup Plugin tools.
- `plugin:manage` allows development/management operations that can start or
change local Plugin processes, currently check and reload. It does not imply
`plugin:invoke`.
- None of these scopes is part of the direct shared-key model baseline.
- For the shared-key OAuth bridge, opt in explicitly with
`webcodex connect ... --auth oauth --oauth-local-plugins`.

`mcp:local` does not grant Plugin access, and `plugin:local` does not grant
Runner-owned MCP provider access.
`webcodex connect ... --auth oauth --oauth-local-plugins`; that opt-in grants
only `plugin:inspect` + `plugin:invoke`, never `plugin:manage`.

`mcp:local` does not grant Plugin access, and Plugin scopes do not grant
Runner-owned MCP provider access. Effectful Plugin operations also pass the
same Workflow Session guard and authority-mode permission policy as other
consequential WebCodex execution when an explicit `recording_session_id` is
supplied; WebCodex never infers that Session from MCP transport identity.

## Troubleshooting

Expand Down
20 changes: 13 additions & 7 deletions docs/PLUGINS.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ WebCodex 会 fail closed 并 retire 该 provider instance。

`plugin_tool call` 必须使用前一次 `describe` 返回的 opaque binding。binding 是
Server 端有界保存的一次 exact observation,不是 bearer authorization token:每次 call
仍然重新要求当前 credential 具有 `plugin:local`,并且当前 caller 仍有权访问对应 logical
仍然重新要求当前 credential 具有 `plugin:invoke`,并且当前 caller 仍有权访问对应 logical
Runner。binding 也可能因为容量上限被 eviction。Runner/provider instance 被替换、tool 被
删除或 schema 改变时,旧 binding 以 `NotStarted` fail closed,必须重新 describe;
WebCodex 不会把它 re-resolve 到新的同名 provider/tool,不会自动生成新 binding,也不会
Expand All @@ -297,15 +297,21 @@ registration catalog。

## OAuth

Native Plugin 使用独立 authority:`plugin:local`。
Native Plugin authority 按 operation 拆分:

- credential 没有 `plugin:local` 时,MCP `tools/list` 不显示 `plugin_tool` 和 startup
Plugin 一级工具,伪造 direct call 也会被拒绝;
- `plugin:local` 不属于 shared-key OAuth baseline;
- `plugin:inspect` 允许 list、describe 等纯 metadata observation;
- `plugin:invoke` 允许 `plugin_tool call` 和 startup Plugin 一级工具;
- `plugin:manage` 允许会启动或改变本地 Plugin process 的开发/管理操作,目前是 check 和
reload;它本身不会隐式授予 `plugin:invoke`;
- 以上 scope 都不属于 direct shared-key model baseline;
- 使用 shared-key OAuth bridge 时,需要显式
`webcodex connect ... --auth oauth --oauth-local-plugins`。
`webcodex connect ... --auth oauth --oauth-local-plugins`;该 opt-in 只授予
`plugin:inspect` + `plugin:invoke`,绝不会授予 `plugin:manage`。

`mcp:local` 不授予 Plugin 权限;`plugin:local` 也不授予 Runner-owned MCP provider 权限。
`mcp:local` 不授予 Plugin 权限;Plugin scopes 也不授予 Runner-owned MCP provider 权限。
effectful Plugin operation 如果携带显式 `recording_session_id`,还必须通过与其他
consequential WebCodex execution 相同的 Workflow Session guard 和 authority-mode
permission policy;WebCodex 不会从 MCP transport identity 推断 Workflow Session。

## 排障

Expand Down
5 changes: 3 additions & 2 deletions src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,9 @@ pub use scopes::{
SCOPE_COMMUNICATION_READ, SCOPE_COMPUTER_CLIPBOARD_READ, SCOPE_COMPUTER_CLIPBOARD_WRITE,
SCOPE_COMPUTER_CONTROL, SCOPE_COMPUTER_DISPLAY_READ, SCOPE_COMPUTER_LAUNCH,
SCOPE_COMPUTER_POINTER_CONTROL, SCOPE_COMPUTER_READ, SCOPE_JOB_RUN, SCOPE_MCP_LOCAL,
SCOPE_MEMORY_MANAGE, SCOPE_MEMORY_READ, SCOPE_PLUGIN_LOCAL, SCOPE_PROJECT_READ,
SCOPE_PROJECT_WRITE, SCOPE_RUNTIME_READ, SCOPE_SESSION_COLLABORATE, SCOPE_SSH_LOCAL,
SCOPE_MEMORY_MANAGE, SCOPE_MEMORY_READ, SCOPE_PLUGIN_INSPECT, SCOPE_PLUGIN_INVOKE,
SCOPE_PLUGIN_MANAGE, SCOPE_PROJECT_READ, SCOPE_PROJECT_WRITE, SCOPE_RUNTIME_READ,
SCOPE_SESSION_COLLABORATE, SCOPE_SSH_LOCAL,
};
#[cfg(test)]
pub use scopes::{SCOPE_ACCOUNT_MANAGE, SCOPE_JOB_DETACH};
Expand Down
5 changes: 3 additions & 2 deletions src/auth/scopes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ pub use webcodex_core::authority::{
SCOPE_COMPUTER_CLIPBOARD_READ, SCOPE_COMPUTER_CLIPBOARD_WRITE, SCOPE_COMPUTER_CONTROL,
SCOPE_COMPUTER_DISPLAY_READ, SCOPE_COMPUTER_LAUNCH, SCOPE_COMPUTER_POINTER_CONTROL,
SCOPE_COMPUTER_READ, SCOPE_JOB_DETACH, SCOPE_JOB_RUN, SCOPE_MCP_LOCAL, SCOPE_MEMORY_MANAGE,
SCOPE_MEMORY_READ, SCOPE_PLUGIN_LOCAL, SCOPE_PROJECT_READ, SCOPE_PROJECT_WRITE,
SCOPE_RUNTIME_READ, SCOPE_SESSION_COLLABORATE, SCOPE_SSH_LOCAL,
SCOPE_MEMORY_READ, SCOPE_PLUGIN_INSPECT, SCOPE_PLUGIN_INVOKE, SCOPE_PLUGIN_MANAGE,
SCOPE_PROJECT_READ, SCOPE_PROJECT_WRITE, SCOPE_RUNTIME_READ, SCOPE_SESSION_COLLABORATE,
SCOPE_SSH_LOCAL,
};

/// True when `scope` is one of the Runner transport scopes.
Expand Down
Loading
Loading