Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
8055e6f
feat(channels): support local gh authentication
wenshao Aug 3, 2026
6911cb3
Merge branch 'main' into feat/github-channel-local-gh-auth
wenshao Aug 3, 2026
8807505
test(channels): align registry catalog test and visuals with optional…
qwen-code-ci-bot Aug 3, 2026
8e8e903
fix(channels): address review feedback for GitHub local gh auth (#8461)
qwen-code-ci-bot Aug 3, 2026
802c0f4
Merge branch 'main' into feat/github-channel-local-gh-auth
qwen-code-dev-bot Aug 3, 2026
549bc39
Merge branch 'main' into feat/github-channel-local-gh-auth
wenshao Aug 3, 2026
55edd76
fix(channels): address second-round review feedback for GitHub local …
qwen-code-ci-bot Aug 4, 2026
7deb8b4
fix(channels): address third-round review feedback for GitHub local g…
qwen-code-ci-bot Aug 4, 2026
a1acfba
Merge branch 'main' into feat/github-channel-local-gh-auth
wenshao Aug 4, 2026
90c4540
fix(channels): address fourth-round review feedback for GitHub local …
qwen-code-ci-bot Aug 4, 2026
1e9d8df
Merge branch 'main' into feat/github-channel-local-gh-auth
qwen-code-dev-bot Aug 4, 2026
fb29a25
fix(channels): address fifth-round review feedback for GitHub local g…
qwen-code-dev-bot Aug 4, 2026
a3868f1
fix(channels): address sixth-round review feedback for GitHub local g…
qwen-code-dev-bot Aug 4, 2026
d176b92
fix(channels): address seventh-round review feedback for GitHub local…
qwen-code-dev-bot Aug 5, 2026
b313746
fix(channels): address eighth-round review feedback for GitHub local …
qwen-code-dev-bot Aug 5, 2026
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
62 changes: 62 additions & 0 deletions docs/design/github-channel-gh-auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# GitHub Channel local `gh` authentication

## Problem

The GitHub Channel currently requires a classic personal access token in every configuration. This prevents Web Shell users from creating a channel that reuses the GitHub CLI authentication already available to the daemon host through `gh auth login`.

The separate Web Shell pull-request integration already relies on the daemon host's `gh` installation and authentication, but the Channel adapter passes only its configured `token` to Octokit.

## Proposed behavior

- Keep an explicitly configured channel token as the highest-priority credential.
- Add an explicit `useLocalGh` opt-in for reusing the daemon host's account-wide GitHub CLI credential.
- When the token is absent and `useLocalGh` is enabled, resolve a token by running `gh auth token --hostname <host>` in the Channel worker.
- Reject configurations that provide neither an explicit token nor the opt-in.
- Use `github.com` as the local `gh` authentication hostname for the default `https://api.github.com` API URL.
- Derive the hostname from a configured GitHub Enterprise `baseUrl`.
- Require `baseUrl` to use HTTPS before resolving a daemon host credential through local `gh` authentication.
- Fail Channel startup with actionable diagnostics when `gh` is unavailable or the selected host is not authenticated.
- Never persist or expose the token returned by `gh`.

## Changes

### GitHub Channel plugin

Make the managed `token` secret optional, remove it from startup-required fields, and add a `useLocalGh` boolean. Update the descriptions to explain that an explicit classic PAT overrides local GitHub CLI authentication. The plugin's management descriptor validates the resolved configuration during managed upserts and rejects one that provides neither a token nor the opt-in, so the daemon mutation boundary keeps the immediate save-time rejection the required token provided before, while `connect()` still rejects configurations whose runtime credential cannot be resolved.

### GitHub Channel adapter

Resolve credentials during `connect()` before constructing Octokit. Use `execFile` without a shell, a bounded timeout, and a bounded output buffer. Pass the selected hostname as a separate argument. The Channel worker already inherits the daemon's `PATH`, `HOME`, and related environment, so `gh` reads the daemon host's existing login.

### Web Shell

The descriptor-driven editor already supports optional secret and boolean fields. Expose `useLocalGh` and require either a preserved/non-empty token or the explicit opt-in before saving. An existing PAT can be cleared only when local `gh` authentication is selected. Update localized field text accordingly.

### Documentation

Document local `gh auth login` as an explicit opt-in and explicit PAT configuration as an override. Warn that the local credential is account-wide and preserve the recommendation to use a separate bot account because the authenticated account cannot trigger its own channel.

## Files affected

- `packages/channels/github/src/index.ts`
- `packages/channels/github/src/GithubAdapter.ts`
- `packages/channels/github/src/GithubAdapter.test.ts`
- `packages/cli/src/commands/channel/channel-registry.test.ts`
- `packages/web-shell/client/components/channels/channel-editor-state.ts`
- `packages/web-shell/client/components/channels/channel-editor-state.test.ts`
- `packages/web-shell/client/components/channels/ChannelEditorDialog.tsx`
- `packages/web-shell/client/e2e/visuals/screenshots.spec.ts`
- `packages/web-shell/client/i18n.tsx`
Comment on lines +46 to +49

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] "Files affected" lists 6 files, but this PR changes 11 — and asymmetrically: it lists channel-editor-state.test.ts without its implementation file. Missing: channel-editor-state.ts (where the "token or useLocalGh" rule and the blank-replacement→clear behavior live), ChannelEditorDialog.tsx, channel-registry.test.ts, and screenshots.spec.ts. Sibling design docs use this section as the change footprint.

Failure scenario: a maintainer using this doc to locate, modify, or revert the feature finds the tests but misses the editor-side enforcement implementation entirely.

Suggested change
- `packages/web-shell/client/components/channels/channel-editor-state.test.ts`
- `packages/web-shell/client/i18n.tsx`
- `packages/web-shell/client/components/channels/channel-editor-state.ts`
- `packages/web-shell/client/components/channels/channel-editor-state.test.ts`
- `packages/web-shell/client/components/channels/ChannelEditorDialog.tsx`
- `packages/cli/src/commands/channel/channel-registry.test.ts`
- `packages/web-shell/client/e2e/visuals/screenshots.spec.ts`
- `packages/web-shell/client/i18n.tsx`
中文说明

“Files affected” 只列出 6 个文件,而本 PR 改动了 11 个——且不对称:列出了 channel-editor-state.test.ts 却没有其实现文件。遗漏:channel-editor-state.ts(“token 或 useLocalGh” 规则与空白替换→clear 行为所在的文件)、ChannelEditorDialog.tsxchannel-registry.test.tsscreenshots.spec.ts。同类设计文档都把该节当作变更足迹使用。

失败场景:维护者依据本文档定位、修改或回退该特性时,能找到测试,却完全漏掉编辑器侧的强制实现。

建议修复:补全缺失文件(见上方 suggestion 块)。

— qwen3.8-max via Qwen Code /review (v0.21.3)

- `docs/users/features/channels/github.md`
- `docs/design/github-channel-gh-auth.md`

## Scope boundaries

- No automatic login or interactive `gh auth login` invocation.
- No GitHub App or fine-grained PAT support.
- No shared cross-package GitHub credential abstraction.
- No change to GitLab Channel authentication.

## Security considerations

The resolved token stays in memory and is passed only to Octokit. It is not written into settings or logs. The subprocess uses fixed arguments and no shell. Existing sender-policy and self-authored-comment protections remain unchanged.
50 changes: 31 additions & 19 deletions docs/users/features/channels/github.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,31 @@ This guide covers setting up a Qwen Code channel that monitors GitHub notificati

## Prerequisites

- A GitHub account for the channel. Use a dedicated bot account when the PAT
owner also needs to operate the channel.
- A GitHub Personal Access Token (PAT) with `notifications` and `public_repo` (or `repo`) scopes
- A GitHub account authenticated with the permissions needed to read notifications and post comments
- The [GitHub CLI](https://cli.github.com/) installed on the host running Qwen Code when using local `gh` authentication

## Creating a Token
Use a dedicated bot account when the authenticated account also needs to operate the channel. GitHub does not generate a usable notification for the account's own activity, and the adapter ignores its own comments to prevent reply loops.

1. Go to **Settings → Developer settings → Personal access tokens → Tokens (classic)**
2. Generate a token with these scopes:
- **notifications** — read notification threads
- **public_repo** (or **repo** for private repos) — post comments
3. Save the token securely as an environment variable
## Authentication

To reuse the GitHub CLI login on the Qwen Code host, authenticate `gh` and explicitly set `useLocalGh: true` in the channel configuration:

```bash
gh auth login
```

Local `gh` authentication is account-wide and may expose notifications from every repository visible to that GitHub account. Enable it only when the workspace operator is trusted to use that account. Otherwise, configure a dedicated PAT.

For GitHub Enterprise Server, authenticate the same host used by `baseUrl`:

```bash
gh auth login --hostname github.example.com
```

You can instead configure a classic personal access token (PAT). An explicit `token` overrides local `gh` authentication. The PAT needs these scopes:

- **notifications** — read notification threads
- **public_repo** (or **repo** for private repos) — post comments

## Configuration

Expand All @@ -25,7 +39,7 @@ Add the channel to `~/.qwen/settings.json`:
"channels": {
"my-github": {
"type": "github",
"token": "$GITHUB_TOKEN",
"useLocalGh": true,
"pollInterval": 60000,
"reasonFilter": ["mention", "review_requested", "assign"],
"senderPolicy": "allowlist",
Expand All @@ -42,18 +56,13 @@ Add the channel to `~/.qwen/settings.json`:
}
```

Set the token as an environment variable:
To override local `gh` authentication with a PAT, add `"token": "$GITHUB_TOKEN"` to the channel and set the environment variable before starting Qwen Code:

```bash
export GITHUB_TOKEN="ghp_your_token_here"
```

The PAT owner cannot trigger its own channel: GitHub self-activity does not
provide a usable notification, and the adapter intentionally ignores its own
comments to prevent reply loops. If the PAT owner needs to operate the channel,
use a separate bot-owned PAT and put only operator accounts in `allowedUsers`.
Startup rejects an allowlist containing only the PAT owner and warns when the
PAT owner appears alongside other operators.
The authenticated account cannot trigger its own channel. If that account needs to operate the channel, authenticate a separate bot account and put only operator accounts in `allowedUsers`. Startup rejects an allowlist containing only the authenticated account and warns when it appears alongside other operators.

### GitHub Enterprise

Expand All @@ -65,11 +74,14 @@ For GitHub Enterprise Server, set `baseUrl`:
}
```

Local `gh` authentication requires an HTTPS `baseUrl` so the daemon host credential cannot be sent over plaintext HTTP.

## Configuration Options

| Option | Default | Description |
| ------------------------- | ------------------------ | --------------------------------------------------------------------------------------------- |
| `token` | (required) | Classic PAT with `notifications` scope |
| `token` | unset | Optional classic PAT with `notifications` scope; overrides local `gh` authentication |
| `useLocalGh` | `false` | Explicitly reuse the daemon host's account-wide GitHub CLI authentication |
| `pollInterval` | `60000` | Poll interval in ms |
| `baseUrl` | `https://api.github.com` | API base URL (for GHE) |
| `groupPolicy` | `"disabled"` | Must be `"open"` for notifications to flow |
Expand Down Expand Up @@ -141,7 +153,7 @@ not retried automatically because GitHub may have created the comment.
- If a user marks a notification as read on github.com before the bot's poll cycle, the bot will not process it.
- The bot does not read comments before the current polling window; `author` and `comment` notifications may aggregate up to 20 comments from that window.
- Inline PR review comments and review summary bodies are not enumerated; only issue/PR comments are processed.
- Requires a classic PAT with `notifications` scope. Fine-grained PATs do not support the notifications API.
- The selected credential must support the Notifications API. Fine-grained PATs do not support it; use local `gh` authentication or a classic PAT with `notifications` scope.

## Starting the Channel

Expand Down
9 changes: 9 additions & 0 deletions packages/channels/base/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,15 @@ export interface ChannelConfigFieldDescriptor {

export interface ChannelManagementDescriptor {
fields: readonly ChannelConfigFieldDescriptor[];

/**
* Cross-field validation applied to the resolved config during managed
* upserts, after secret updates. Return an error message to reject the
* update, or undefined to accept it.
*/
validateConfig?: (
config: Readonly<Record<string, unknown>>,
) => string | undefined;
Comment on lines +410 to +412

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R9-2: This new plugin hook receives raw secret values in config, and its returned message is user-visible — the contract should say so.
In WorkspaceChannelSettingsStore.upsert, stored secrets are merged into nextConfig before validateConfig runs, so the hook sees raw secret values; the returned message reaches web-shell clients in the HTTP 400 body. redactLogCredentials only strips well-known secret formats (ghp_/gho_/Bearer/token= etc.), so arbitrary secrets pass through. The GitHub implementation is safe today (fixed message), but nothing in this public plugin contract forbids a future or extension plugin from echoing a config value into the rejection message.

Failure scenario: a channel plugin echoes config['token'] into its validateConfig return string; every client that can trigger a save receives the stored secret in the 400 body — a value the snapshot API deliberately redacts to {present, source}.

Suggested contract hardening (extend the JSDoc above this signature):

/**
 * ...
 * `config` may contain raw secret values; the returned message is
 * user-visible and must not include any config value.
 */
中文说明

[Suggestion] 这个新的插件 hook 会在 config 中收到原始 secret 值,且其返回信息对用户可见——契约应当明示这一点。
WorkspaceChannelSettingsStore.upsert 中,已存储的 secret 会先合并进 nextConfig 再执行 validateConfig,因此 hook 能看到原始 secret 值;返回信息会随 HTTP 400 响应体到达 web-shell 客户端。redactLogCredentials 只剥离已知格式的密钥(ghp_/gho_/Bearer/token= 等),任意 secret 会原样透传。当前 GitHub 实现是安全的(固定文案),但这份公开插件契约没有任何条款禁止未来或扩展插件把配置值回显到拒绝信息里。

失败场景:某 channel 插件把 config['token'] 回显进 validateConfig 的返回字符串;任何能触发保存的客户端都会在 400 响应体中收到已存储的密钥——而快照 API 刻意将该值脱敏为 {present, source}

建议补充契约:在上方 JSDoc 中追加"config 可能包含原始 secret 值;返回信息对用户可见,不得包含任何配置值"。

— qwen3.8-max via Qwen Code /review (v0.21.5)

}

/**
Expand Down
Loading
Loading