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
8 changes: 7 additions & 1 deletion crates/webcodex-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,13 @@ use webcodex_cli::{
OpsSmokePreflightOptions, ServerStatusOptions, ServiceControl, StatusOptions,
AGENT_SERVICE_UNIT, DEFAULT_LOG_LINES, SERVER_SERVICE_FILE, SERVER_SERVICE_UNIT,
};
const SETUP_GPT_SCOPES: &[&str] = &["runtime:read", "project:read", "project:write", "job:run"];
const SETUP_GPT_SCOPES: &[&str] = &[
"runtime:read",
"session:collaborate",
"project:read",
"project:write",
"job:run",
];
const SETUP_AGENT_SCOPES: &[&str] = &[
"agent:register",
"agent:poll",
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 @@ -32,6 +32,7 @@ const OAUTH_SECRET_DISCLOSED_FILE_PREFIX: &str = ".oauth-client-secret-disclosed
// client until this list is explicitly reviewed and changed.
const HOSTED_CONNECT_OAUTH_SCOPES: &[&str] = &[
"runtime:read",
"session:collaborate",
"project:read",
"project:write",
"job:run",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const LOCAL_MCP_SCOPE: &str = "mcp:local";
const CODING_AGENT_SCOPE: &str = "coding_agent:run";
const BRIDGE_BASELINE_SCOPES: &[&str] = &[
"runtime:read",
"session:collaborate",
"project:read",
"project:write",
"job:run",
Expand All @@ -32,6 +33,7 @@ const BRIDGE_OPTIONAL_COMPUTER_SCOPES: &[&str] = &[
// retain a narrower non-empty baseline subset plus all optional Computer scopes.
const BRIDGE_COMPUTER_ENABLED_SCOPES: &[&str] = &[
"runtime:read",
"session:collaborate",
"project:read",
"project:write",
"job:run",
Expand Down Expand Up @@ -623,6 +625,7 @@ mod tests {
async fn ordinary_connect_oauth_provisions_then_reuses_same_shared_key_client() {
let scopes = vec![
"runtime:read",
"session:collaborate",
"project:read",
"project:write",
"job:run",
Expand All @@ -648,6 +651,7 @@ mod tests {
"redirect_uri": "https://chatgpt.example/callback",
"allowed_scopes": [
"runtime:read",
"session:collaborate",
"project:read",
"project:write",
"job:run",
Expand Down
15 changes: 9 additions & 6 deletions docs/AUTH_MODEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ A shared key is not an admin credential, not a managed user identity, and not
production IAM. It has no independent per-device revocation: rotate the shared
secret for the whole group, or use managed credentials.

Its default principal carries `runtime:read`, `project:read`, `project:write`,
`job:run`, `computer:read`, `computer:control`, and the bounded Agent-transport
Its default principal carries `runtime:read`, `session:collaborate`, `project:read`,
`project:write`, `job:run`, `computer:read`, `computer:control`, and the bounded Agent-transport
scopes `agent:register`, `agent:poll`, `agent:result`, and `agent:job_update`. It
does not carry account-management or admin scopes.

Expand Down Expand Up @@ -133,7 +133,10 @@ Use `wc_pat_xxx` for:
Supply it to CLI commands with `--token-file <path>` rather than `--token`, so
the value stays out of shell history and process lists. Scope the PAT to the
workflow. For example, a GPT Action that inspects and edits projects may need
`runtime:read`, `project:read`, `project:write`, and `job:run`.
`runtime:read`, `project:read`, `project:write`, and `job:run`; add
`session:collaborate` only when it must post, resolve, complete, replace, withdraw,
or close Workflow Session collaboration state. `runtime:read` alone remains
observation-only for that collaboration state.

## `wc_agent_xxx` (Runner token)

Expand Down Expand Up @@ -181,15 +184,15 @@ An OAuth client's `allowed_scopes` is a registration-time delegation ceiling and
is never automatically widened when WebCodex adds a new permission such as
`computer:control` or `computer:launch`. The omitted-or-empty default is the
explicit closed legacy permission set that existed before application launch; it
does not track the global supported-scope registry. `computer:launch` and future
permission scopes therefore require explicit opt-in. First-party operators may explicitly replace an active client's
does not track the global supported-scope registry. `session:collaborate`,
`computer:launch`, and future permission scopes therefore require explicit opt-in. First-party operators may explicitly replace an active client's
complete allow-list with `POST /api/oauth/clients/update_scopes`. A real change
atomically revokes that client's existing access tokens, refresh tokens, and
outstanding authorization codes, so the client must complete OAuth authorization
again before using the new scope set.
`computer:display_read`, `computer:pointer_control`, `computer:clipboard_read`, and `computer:clipboard_write` follow that rule: all are outside the frozen legacy default and are available only through explicit client scope opt-in.

`webcodex connect <server> --auth oauth` is the ordinary hosted shared-key OAuth bridge. The OAuth client is owned by the direct shared key's SHA-256 group hash, and authorization codes, access tokens, and refresh tokens carry that same `shared_key_hash` subject binding. Direct shared-key bearer authority remains the explicit baseline `runtime:read`, `project:read`, `project:write`, `job:run`, `computer:read`, and `computer:control`. A fresh ordinary OAuth client starts with that full baseline, while an existing protected client may legitimately carry any non-empty unique subset of that baseline. `--oauth-computer-permissions` changes only the OAuth client ceiling by appending the fixed closed set `computer:launch`, `computer:display_read`, `computer:pointer_control`, `computer:clipboard_read`, and `computer:clipboard_write` to the client's existing baseline subset; it never restores a baseline scope that was previously absent. A Computer-enabled ceiling is valid only when it contains the complete five-scope optional set, has at least one baseline scope, and contains no scope outside those two explicit closed universes. It never derives from the global OAuth registry and never includes `account:manage`, `admin`, `job:detach`, any `agent:*` transport scope, or future scopes. A ceiling is not a grant: all five optional Computer permissions are unchecked on the WebCodex authorize page, and the authorization code receives only baseline scopes present in the current OAuth request, optional scopes explicitly selected through the fixed permission bundles, and protocol-only `offline_access` when requested. Access tokens and refresh rotation preserve that exact grant set. Launch selection requires the request to contain both `computer:read` and `computer:launch`; the launch permission bundle itself still adds only `computer:launch`. Other optional permissions likewise require their complete runtime request prerequisites, and WebCodex never fills in a missing requested scope. Reconnect without the flag never widens an existing baseline client; revoked/missing-client replacement preserves the protected baseline subset recorded in `previous_allowed_scopes`. If explicit opt-in actually changes an existing shared-key-owned client's ceiling, WebCodex atomically revokes its access tokens, refresh tokens, and outstanding codes so browser authorization must run again. The Runner continues using the direct shared key, OAuth access tokens remain forbidden on Agent transport, and `--auth managed-oauth` remains the separate managed-user flow.
`webcodex connect <server> --auth oauth` is the ordinary hosted shared-key OAuth bridge. The OAuth client is owned by the direct shared key's SHA-256 group hash, and authorization codes, access tokens, and refresh tokens carry that same `shared_key_hash` subject binding. Direct shared-key bearer authority remains the explicit baseline `runtime:read`, `session:collaborate`, `project:read`, `project:write`, `job:run`, `computer:read`, and `computer:control`. A fresh ordinary OAuth client starts with that full baseline, while an existing protected client may legitimately carry any non-empty unique subset of that baseline. `--oauth-computer-permissions` changes only the OAuth client ceiling by appending the fixed closed set `computer:launch`, `computer:display_read`, `computer:pointer_control`, `computer:clipboard_read`, and `computer:clipboard_write` to the client's existing baseline subset; it never restores a baseline scope that was previously absent. A Computer-enabled ceiling is valid only when it contains the complete five-scope optional set, has at least one baseline scope, and contains no scope outside those two explicit closed universes. It never derives from the global OAuth registry and never includes `account:manage`, `admin`, `job:detach`, any `agent:*` transport scope, or future scopes. A ceiling is not a grant: all five optional Computer permissions are unchecked on the WebCodex authorize page, and the authorization code receives only baseline scopes present in the current OAuth request, optional scopes explicitly selected through the fixed permission bundles, and protocol-only `offline_access` when requested. Access tokens and refresh rotation preserve that exact grant set. Launch selection requires the request to contain both `computer:read` and `computer:launch`; the launch permission bundle itself still adds only `computer:launch`. Other optional permissions likewise require their complete runtime request prerequisites, and WebCodex never fills in a missing requested scope. Reconnect without the flag never widens an existing baseline client; revoked/missing-client replacement preserves the protected baseline subset recorded in `previous_allowed_scopes`. If explicit opt-in actually changes an existing shared-key-owned client's ceiling, WebCodex atomically revokes its access tokens, refresh tokens, and outstanding codes so browser authorization must run again. The Runner continues using the direct shared key, OAuth access tokens remain forbidden on Agent transport, and `--auth managed-oauth` remains the separate managed-user flow.

The shared-key authorize page evaluates optional Computer availability only for an exact shared-key-owned client whose `owner_shared_key_hash` matches the submitted key and whose matching Runner group is online. Each permission is considered available only when one same online Runner advertises the full capability requirement; capabilities are never unioned across Runners. POST recomputes that availability, so a capability loss between GET and consent fails closed without creating a code. These checks are backend capability checks only: authorization performs no hidden display observation, pointer/clipboard effect, launch, or OS-permission probe. Native/OS preflight remains the runtime tool call's responsibility. For OAuth-authenticated MCP callers, `tools/list` is projected through the same runtime scope policy as `tools/call`, so high-privilege Computer tools are absent when the actual token grant lacks their required scopes; the runtime gate remains authoritative even for a forged direct call.

Expand Down
8 changes: 5 additions & 3 deletions docs/AUTH_MODEL.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,9 @@ agent 连接 token。

用 `--token-file <path>` 提供给 CLI 命令,而不是 `--token`,以免进入 shell 历史。
按工作流限制 PAT scope。例如,一个要检查和编辑项目的 GPT Action 可能需要
`runtime:read`、`project:read`、`project:write`、`job:run`。
`runtime:read`、`project:read`、`project:write`、`job:run`;只有需要 post、resolve、
complete、replace、withdraw 或 close Workflow Session 协作状态时才额外授予
`session:collaborate`。仅有 `runtime:read` 对这类协作状态保持只读观察能力。

## `wc_agent_xxx`(Runner 令牌)

Expand Down Expand Up @@ -146,13 +148,13 @@ Server 支持 authorization-code grant、token 撤销与 OAuth metadata。动态
[部署指南](DEPLOYMENT.zh-CN.md#oauth2)。

OAuth client 的 `allowed_scopes` 是 client 注册时确定的委派权限上限;WebCodex 后续
增加 `computer:control` 之类的新权限时,不会自动给历史 client 扩权。first-party
增加 `session:collaborate`、`computer:control` 之类的新权限时,不会自动给历史 client 扩权。first-party
operator 可以通过 `POST /api/oauth/clients/update_scopes` 显式替换 active client 的
完整 allowlist。allowlist 真正变化时,Server 会在同一事务里撤销该 client 现有的
access token、refresh token 与尚存 authorization code,因此 client 必须重新完成
OAuth 授权,才能使用新的 scope 集合。

`webcodex connect <server> --auth oauth` 是普通 hosted shared-key OAuth bridge。OAuth client 归属于 direct shared key 的 SHA-256 group hash,authorization code、access token、refresh token 都保留同一个 `shared_key_hash` subject binding。direct shared-key bearer authority 始终保持显式 baseline:`runtime:read`、`project:read`、`project:write`、`job:run`、`computer:read`、`computer:control`。fresh ordinary OAuth client 从完整 baseline 开始;已有受保护 client 则可以合法持有该 baseline 的任意 non-empty、unique subset。只有 connect 显式传 `--oauth-computer-permissions`,OAuth client ceiling 才在**现有 baseline subset**上追加固定 closed set:`computer:launch`、`computer:display_read`、`computer:pointer_control`、`computer:clipboard_read`、`computer:clipboard_write`;绝不会恢复此前缺失的 baseline scope。Computer-enabled ceiling 只有在完整包含这五个 optional scopes、至少保留一个 baseline scope,且不存在两个显式 closed universe 之外的 scope 时才合法。该集合绝不从全局 OAuth registry 自动派生,也永远不包含 `account:manage`、`admin`、`job:detach`、任何 `agent:*` transport scope 或未来新增 scope。client ceiling 不是实际 grant:WebCodex authorize 页面五项 optional Computer permission 默认全部未勾选,authorization code 只包含本次 request 中实际授予的 baseline scope、用户明确选择后由固定 bundle 映射的 optional scope,以及本次协议需要的 `offline_access`。access token 与 refresh rotation 逐字保留该实际 grant,不重新扩到 client ceiling。Launch selection 要求本次 request 同时包含 `computer:read` 与 `computer:launch`,但 launch permission bundle 自身仍只新增 `computer:launch`;其他 optional permission 同样要求其完整 runtime request prerequisite,Server 不会替 client 偷偷补 request 未包含的 scope。普通 reconnect 不会扩已有 baseline client;revoked/missing client replacement 会按 `previous_allowed_scopes` 保留受保护 baseline subset。只有显式 opt-in 真正改变 shared-key-owned client ceiling 时,才原子撤销其 access/refresh token 与未使用 authorization code,要求重新完成浏览器授权。Runner 继续使用 direct shared key,OAuth access token 永远不能用于 Agent transport;`--auth managed-oauth` 仍是独立 managed-user 流程。
`webcodex connect <server> --auth oauth` 是普通 hosted shared-key OAuth bridge。OAuth client 归属于 direct shared key 的 SHA-256 group hash,authorization code、access token、refresh token 都保留同一个 `shared_key_hash` subject binding。direct shared-key bearer authority 始终保持显式 baseline:`runtime:read`、`session:collaborate`、`project:read`、`project:write`、`job:run`、`computer:read`、`computer:control`。fresh ordinary OAuth client 从完整 baseline 开始;已有受保护 client 则可以合法持有该 baseline 的任意 non-empty、unique subset。只有 connect 显式传 `--oauth-computer-permissions`,OAuth client ceiling 才在**现有 baseline subset**上追加固定 closed set:`computer:launch`、`computer:display_read`、`computer:pointer_control`、`computer:clipboard_read`、`computer:clipboard_write`;绝不会恢复此前缺失的 baseline scope。Computer-enabled ceiling 只有在完整包含这五个 optional scopes、至少保留一个 baseline scope,且不存在两个显式 closed universe 之外的 scope 时才合法。该集合绝不从全局 OAuth registry 自动派生,也永远不包含 `account:manage`、`admin`、`job:detach`、任何 `agent:*` transport scope 或未来新增 scope。client ceiling 不是实际 grant:WebCodex authorize 页面五项 optional Computer permission 默认全部未勾选,authorization code 只包含本次 request 中实际授予的 baseline scope、用户明确选择后由固定 bundle 映射的 optional scope,以及本次协议需要的 `offline_access`。access token 与 refresh rotation 逐字保留该实际 grant,不重新扩到 client ceiling。Launch selection 要求本次 request 同时包含 `computer:read` 与 `computer:launch`,但 launch permission bundle 自身仍只新增 `computer:launch`;其他 optional permission 同样要求其完整 runtime request prerequisite,Server 不会替 client 偷偷补 request 未包含的 scope。普通 reconnect 不会扩已有 baseline client;revoked/missing client replacement 会按 `previous_allowed_scopes` 保留受保护 baseline subset。只有显式 opt-in 真正改变 shared-key-owned client ceiling 时,才原子撤销其 access/refresh token 与未使用 authorization code,要求重新完成浏览器授权。Runner 继续使用 direct shared key,OAuth access token 永远不能用于 Agent transport;`--auth managed-oauth` 仍是独立 managed-user 流程。

shared-key authorize 页面只对 exact shared-key-owned client 做 optional Computer picker,并要求 `owner_shared_key_hash` 与提交的 shared key 精确匹配、对应 Runner group 在线。每个 permission 只有在**同一个在线 Runner**同时满足完整 capability requirement 时才显示 available,绝不把多个 Runner 的 capability 做 union;POST 会重新计算,因此 GET 后 capability 消失会 fail closed 且不创建 code。这里的 capability 只代表 WebCodex backend 当前支持,不代表 OS/native permission 或操作一定成功;authorize 过程中不会执行隐藏 display observation、pointer/clipboard effect、launch 或 OS-permission probe,runtime tool call 仍负责最新 native/OS preflight。

Expand Down
1 change: 1 addition & 0 deletions src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ pub use scopes::{
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_PROJECT_READ, SCOPE_PROJECT_WRITE, SCOPE_RUNTIME_READ,
SCOPE_SESSION_COLLABORATE,
};
#[cfg(test)]
pub use scopes::{SCOPE_ACCOUNT_MANAGE, SCOPE_JOB_DETACH};
Expand Down
6 changes: 5 additions & 1 deletion src/auth/project_share.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@
//! old authorization codes/access tokens/refresh tokens unusable without
//! changing ownership of existing project tasks.

use super::{SCOPE_JOB_RUN, SCOPE_PROJECT_READ, SCOPE_PROJECT_WRITE, SCOPE_RUNTIME_READ};
use super::{
SCOPE_JOB_RUN, SCOPE_PROJECT_READ, SCOPE_PROJECT_WRITE, SCOPE_RUNTIME_READ,
SCOPE_SESSION_COLLABORATE,
};

pub(crate) const PROJECT_SHARE_OAUTH_SUBJECT_KIND: &str = "project_share";
pub(crate) const PROJECT_SHARE_OAUTH_TOKEN_KIND: &str = "oauth2_project";
pub(crate) const PROJECT_SHARE_SESSION_PREFIX: &str = "wc_share_";
pub(crate) const PROJECT_SHARE_OAUTH_SCOPES: &[&str] = &[
SCOPE_RUNTIME_READ,
SCOPE_SESSION_COLLABORATE,
SCOPE_PROJECT_READ,
SCOPE_PROJECT_WRITE,
SCOPE_JOB_RUN,
Expand Down
Loading
Loading