Skip to content

Commit 92ee722

Browse files
chore: merge origin/main into feat/optional-worktree
Resolve the collision between optional branch-session worktrees and the git remotes management that landed on main (QwenLM#11163): - git-branches: keep both the transport-key-aware gitRemoteEnv and the now-exported runGit, and hold GIT_ALLOW_PROTOCOL out of the case-insensitive git_* scrub so main's helper-protocol normalization still sees the inherited value instead of reading an already-deleted key. - BranchPickerPopover: the workspace-reset effect now clears main's remotes request/focus/sticky state and re-runs on gitSessionId as well as gitCwd. - Tests: union both sides' hostile-env inputs and mount() prop surface.
2 parents ac9320a + 3987391 commit 92ee722

232 files changed

Lines changed: 31444 additions & 3077 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/.size-baseline‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
11394 finalize-release.yml
3131
16647 live-host-release.yml
3232
5950 live-host.yml
33+
1343 mobile-shell.yml
3334
7642 main-ci-failure-issue.yml
3435
1686 npm-cache.yml
3536
2709 pnpm-lock-freshness.yml

‎.github/workflows/mobile-shell.yml‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: 'Android Mobile Shell'
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- '.github/workflows/mobile-shell.yml'
7+
- 'packages/mobile-shell/**'
8+
- 'scripts/check-desktop-isolation.js'
9+
push:
10+
branches:
11+
- 'main'
12+
paths:
13+
- '.github/workflows/mobile-shell.yml'
14+
- 'packages/mobile-shell/**'
15+
- 'scripts/check-desktop-isolation.js'
16+
workflow_dispatch:
17+
18+
permissions:
19+
contents: 'read'
20+
21+
jobs:
22+
validate:
23+
name: 'Build, unit tests, and lint'
24+
runs-on: 'ubuntu-latest'
25+
timeout-minutes: 20
26+
defaults:
27+
run:
28+
working-directory: 'packages/mobile-shell'
29+
steps:
30+
- name: 'Checkout'
31+
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
32+
33+
- name: 'Set up Java 17'
34+
uses: 'actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3' # v4
35+
with:
36+
distribution: 'temurin'
37+
java-version: '17'
38+
39+
- name: 'Verify Gradle wrapper'
40+
shell: 'bash'
41+
run: |
42+
expected="$(tr -d '[:space:]' < gradle/wrapper/gradle-wrapper.jar.sha256)"
43+
actual="$(sha256sum gradle/wrapper/gradle-wrapper.jar | cut -d ' ' -f1)"
44+
test "$actual" = "$expected"
45+
46+
- name: 'Build, run unit tests, and lint'
47+
shell: 'bash'
48+
run: './gradlew --no-daemon assembleDebug testDebugUnitTest lintDebug'

‎.github/workflows/pnpm-worktree-smoke.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ on:
1313
- 'packages/*/package.json'
1414
- '!packages/desktop-shell/package.json'
1515
- '!packages/live-host/package.json'
16+
- '!packages/mobile-shell/package.json'
1617
- 'packages/channels/*/package.json'
1718
- 'integrations/*/package.json'
1819
- 'patches/**'
@@ -36,6 +37,7 @@ on:
3637
- 'packages/*/package.json'
3738
- '!packages/desktop-shell/package.json'
3839
- '!packages/live-host/package.json'
40+
- '!packages/mobile-shell/package.json'
3941
- 'packages/channels/*/package.json'
4042
- 'integrations/*/package.json'
4143
- 'patches/**'

‎docs/design/2026-09-07-goal-no-progress-pause.md‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,12 @@ better, on the turn where they coincide.
8181
`limitKind`. The bound stands down when `spentBudget` finds any spent
8282
allowance, the streak stays on the record, and the gate runs as it would
8383
have.
84-
- A checkpoint stall streak. A model that fills the evidence window with prose
84+
- A checkpoint stall streak. (Historical: since the verifier judges a proposal
85+
from the transcript tail directly, `update_goal` no longer answers
86+
`checkpointRequired` and no checkpoint runs; the stall streak stays at zero.)
87+
A model that fills the evidence window with prose
8588
and calls no tool is quiet by this bound's measure and overflowing by the
86-
checkpoint's, and `update_goal` answers `checkpointRequired` on an
89+
checkpoint's, and `update_goal` used to answer `checkpointRequired` on an
8790
overflowing catalog without recording a proposal. That Goal is drowning in
8891
evidence, not idling: the pause's remedy (resume) would send it straight
8992
back into the same window, while the stall breaker stops it with the reason
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Named-workflows-only lock
2+
3+
[中文](./2026-09-17-workflow-name-only.zh-CN.md)
4+
5+
Tracking issue: #11013, item 12 (remaining part).
6+
7+
Status: implemented in #12078.
8+
9+
## Problem
10+
11+
A deployment can let the model start the workflows an extension ships (`whenToUse`, #11957) and can scope approvals to a workflow by name or path (#11943). It had no way to stop the same model from writing an inline script of up to 1,000 agents and running it. An inline script has no name or path to write a permission rule against, so once a deployment relaxed approvals for its named workflows, inline scripts went through as well.
12+
13+
## Decisions
14+
15+
### The switch
16+
17+
`tools.workflowNameOnly` (default `false`) or `QWEN_CODE_WORKFLOW_NAME_ONLY=1` turns the lock on. `Config.isWorkflowNameOnly()` decides it once, when the session starts: the Workflow tool builds its description and parameter schema at startup, and a lock that changed under them would leave the model holding a contract the tool no longer honours.
18+
19+
A workspace may turn the lock on but not off. The key is a tighten-only setting: a workspace `true` is honoured, and a workspace `false` under an operator's `true` is dropped with a warning. The environment variable is excluded from project `.env` files, so repository content cannot unset an operator's exported value.
20+
21+
### What is locked
22+
23+
| Source | Locked session | Why |
24+
| ------------------------------------------------------------ | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
25+
| `script` | refused | Code the model writes in the session has no identity a rule can match. |
26+
| `scriptPath` | refused | Every saved workflow is reachable by name; a path adds nothing a name cannot say, and the generated-scripts root is a runtime directory. |
27+
| `name`, and `name` with `resumeFromRunId` | allowed | A `Workflow(name:…[,sha256:…])` rule matches it. |
28+
| `workflow({ scriptPath })` inside a script the model started | refused | The same rule, one level down. `workflow('<name>')` still works. |
29+
30+
A source counts the way parameter validation counts it: an empty `script` or `scriptPath` beside a `name` does not refuse a call that runs by name.
31+
32+
### The lock constrains the model, not the host
33+
34+
The refusal sits in the tool's `build`, the entry every model and client call takes, including the interactive slash command's `schedule_tool`. Runs a host starts over ACP — `run-saved`, `run-script`, retry and rerun — go through `buildSessionOwnedBackground`, which shares parameter validation but not `build`, and are not restricted. The nested refusal follows the same line: the tool asks the runner for it only on a call it built for the model or a client, so a host script may still nest by path.
35+
36+
### What the model is told
37+
38+
A locked session tells the model the rule instead of letting it find the rule by failing:
39+
40+
- The parameter schema omits `script` and `scriptPath`. `name` is not made `required`, because the host's runs validate against the same schema and start from a script.
41+
- The description replaces the authoring pointer with a "Named workflows only" section, and its shared decision and runtime text lose the sentences that send the model to `scriptPath` or to editing a persisted script. A test holds that no such advice survives outside the section.
42+
- The `workflow` keyword reminder steers toward `{ name, args }` instead of authoring a script. The lock is read before anything else in that path can fail.
43+
- The interactive `/<name>` command dispatches by name.
44+
45+
### Resuming by name
46+
47+
Outside the lock, resume calls keep naming the script path, so existing `Workflow(scriptPath:…)` grants keep matching. In a locked session a path is refused, so the resume call names the workflow — but only a name that leads back to the script the run executed. A workflow name can be recorded from a path in a subdirectory, from a user workflow a same-named project workflow shadows, or carried over by a retry that fell back to the run's inline copy; resuming by such a name would run a different script, or none. The runner therefore resolves the name once, as the run starts, and records it as the run's `resumeName` only when it resolves to the same file. A run without one gets no resume call; its failure notice says that only whoever started it can retry it.
48+
49+
The lock itself lives in one place, the Config. The Config hands it to the workflow run registry it owns, which reads it when a notification offers a resume call; the tool and the keyword reminder read the Config.
50+
51+
### Hosts
52+
53+
`workflowToolFeatures.nameOnly` in `GET /session/:id/supported-commands` reports the lock, so a host knows the model is restricted while its own `run-script` still starts runs.
54+
55+
## Differences from Claude Code
56+
57+
Claude Code's `CLAUDE_WORKFLOW_NAME_ONLY` refuses `script`, `scriptPath` and `resumeFromRunId`, limits resolution to built-in workflows, and refuses a nested `workflow({ scriptPath })` for every run. Qwen Code has no built-in workflows, and the lock serves deployments that start named runs and resume them, so it keeps every name tier (project, user, extension), allows a named resume, and leaves host-started runs unrestricted.
58+
59+
## Related fix
60+
61+
The tighten-only merge compared a workspace value against the stricter of User and SystemDefaults, while the merge lets User override SystemDefaults. With SystemDefaults stricter and User looser, a workspace value equal to SystemDefaults was dropped as "no change" although the value in force was User's. The baseline is now the value in force without the workspace: User's when User sets the key, otherwise SystemDefaults', otherwise the default. This also affects `agents.crossSessionMessaging` and `agents.crossSessionInbound`.
62+
63+
## Limits and risks
64+
65+
- The lock makes every run the model starts addressable by a name rule; it does not approve or block anything by itself. The model can still save a new workflow file and run it by name, which an approval rule scoped to specific names or script digests will ask about.
66+
- In a locked session the `/review` workflow fan-out is unavailable, because it runs a generated script by path. The review skill already reports an unavailable tool and stops.
67+
- The resume name is checked when a run starts. A workflow file added or removed afterwards is not reflected.
68+
69+
## Not in scope
70+
71+
- A bundled `deep-research` workflow (the other remaining part of item 12).
72+
- An allowlist of name sources.
73+
- Changing the lock during a session.
74+
75+
## Verification
76+
77+
- Unit tests cover each refused and accepted source, the host runs (script, script path, nested path), the schema and description shapes, the resume name when it matches and when it does not, the notices, the keyword reminder when the registry throws, the slash command, the Config's start-time decision, the tighten-only baseline for this key and for `crossSessionInbound`, and the ACP flag.
78+
- Hand-made mutations of each new branch fail at least one test.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# 仅命名 workflow 锁
2+
3+
[English](./2026-09-17-workflow-name-only.md)
4+
5+
跟踪 issue:#11013 第 12 项(剩余部分)。
6+
7+
状态:已在 #12078 实现。
8+
9+
## 问题
10+
11+
部署方可以让模型启动扩展随包发行的 workflow(`whenToUse`,#11957),也可以按名字或路径限定审批(#11943),但没有办法阻止同一个模型自己写一段最多 1000 个 agent 的内联脚本来运行。内联脚本没有可写权限规则的名字或路径,部署方一旦为命名 workflow 放宽审批,内联脚本也会一并放行。
12+
13+
## 决策
14+
15+
### 开关
16+
17+
`tools.workflowNameOnly`(默认 `false`)或 `QWEN_CODE_WORKFLOW_NAME_ONLY=1` 打开锁。`Config.isWorkflowNameOnly()` 在会话启动时确定一次:Workflow 工具在启动时构建描述和参数 schema,锁若在会话中途变化,模型手里的契约就与工具实际行为不一致。
18+
19+
工作区可以打开锁,不能关闭。该键是"工作区只能收紧"的设置:工作区的 `true` 生效;操作者设为 `true` 时,工作区的 `false` 会被丢弃并告警。环境变量不允许由项目 `.env` 设置,仓库内容无法取消操作者导出的值。
20+
21+
### 锁住什么
22+
23+
| 来源 | 锁开时 | 理由 |
24+
| --------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------ |
25+
| `script` | 拒绝 | 模型在会话里写的代码没有可被规则匹配的身份。 |
26+
| `scriptPath` | 拒绝 | 每个已保存 workflow 都能按名字到达;路径提供不了名字之外的东西,generated-scripts 根目录是运行期目录。 |
27+
| `name`,以及 `name` 加 `resumeFromRunId` | 允许 | 可被 `Workflow(name:…[,sha256:…])` 规则匹配。 |
28+
| 模型启动的脚本里的 `workflow({ scriptPath })` | 拒绝 | 同一条规则的下一层。`workflow('<name>')` 照常。 |
29+
30+
来源的判定与参数校验一致:`name` 旁边为空的 `script` 或 `scriptPath` 不会让一个按名运行的调用被拒。
31+
32+
### 锁约束模型,不约束宿主
33+
34+
拒绝放在工具的 `build` 里,模型和客户端的每次调用都经过这里,包括交互斜杠命令的 `schedule_tool`。宿主经 ACP 发起的 `run-saved`、`run-script`、retry、rerun 走 `buildSessionOwnedBackground`,与 `build` 共用参数校验但不经过 `build`,不受限制。嵌套拒绝也按同一条线划分:只有工具为模型或客户端构建的调用才会要求 runner 拒绝,宿主的脚本仍可按路径嵌套。
35+
36+
### 告诉模型什么
37+
38+
锁开的会话直接把规则告诉模型,而不是让它撞错才知道:
39+
40+
- 参数 schema 去掉 `script` 与 `scriptPath`。`name` 不设为必填,因为宿主的运行复用同一份 schema 校验,并从脚本启动。
41+
- 描述用 "Named workflows only" 一段替换 authoring 指针,共享的决策与运行时文字去掉引导模型使用 `scriptPath` 或编辑已落盘脚本的句子。有测试保证这类说法不会在该段之外残留。
42+
- `workflow` 关键词提醒引导 `{ name, args }`,不再引导写脚本。锁状态在该路径里任何可能失败的步骤之前读取。
43+
- 交互 `/<name>` 命令按名调度。
44+
45+
### 按名恢复
46+
47+
锁关时,恢复调用仍给出脚本路径,已有的 `Workflow(scriptPath:…)` 授权继续匹配。锁开时路径被拒,恢复调用改为给出 workflow 名,但只给能回到本次运行所执行脚本的名字。workflow 名可能来自子目录里的文件路径、来自被同名项目 workflow 遮蔽的用户 workflow,或是 retry 回退到内联副本时沿用的旧名;按这样的名字恢复会运行另一个脚本,或者找不到脚本。因此 runner 在运行启动时解析一次该名字,只有解析到同一个文件时才记为该运行的 `resumeName`。没有 `resumeName` 的运行不提供恢复调用,失败通知说明只能由启动方重试。
48+
49+
锁本身只存在于一处:Config。Config 把它交给自己持有的 workflow 运行 registry,registry 在通知里给出恢复调用时读取;工具和关键词提醒读 Config。
50+
51+
### 宿主
52+
53+
`GET /session/:id/supported-commands` 的 `workflowToolFeatures.nameOnly` 反映锁状态,宿主据此知道模型受限,而自己的 `run-script` 仍能启动运行。
54+
55+
## 与 Claude Code 的差异
56+
57+
Claude Code 的 `CLAUDE_WORKFLOW_NAME_ONLY` 拒绝 `script`、`scriptPath` 和 `resumeFromRunId`,只解析内置 workflow,并对所有运行拒绝嵌套的 `workflow({ scriptPath })`。Qwen Code 没有内置 workflow,而这个锁面向的是启动命名运行并恢复它们的部署,所以保留全部名字层级(项目、用户、扩展),允许按名恢复,并且不限制宿主发起的运行。
58+
59+
## 相关修复
60+
61+
"工作区只能收紧"的合并逻辑把工作区的值与 User、SystemDefaults 中更严的那个比较,而合并时 User 覆盖 SystemDefaults。当 SystemDefaults 更严、User 更松时,与 SystemDefaults 相同的工作区值会被当成"没有变化"丢弃,尽管实际生效的是 User 的值。现在的基线是不含工作区时实际生效的值:User 设置了就用 User 的,否则用 SystemDefaults 的,否则用默认值。这一修复同样影响 `agents.crossSessionMessaging` 与 `agents.crossSessionInbound`。
62+
63+
## 局限与风险
64+
65+
- 锁保证模型发起的每次运行都可被按名规则寻址,它本身不批准也不阻止任何东西。模型仍可保存一个新的 workflow 文件再按名运行,按具体名字或脚本摘要限定的审批规则会对它发起询问。
66+
- 锁开的会话里 `/review` 的 workflow 扇出不可用,因为它按路径运行生成的脚本。review skill 已规定工具不可用时报告并停止。
67+
- 恢复名在运行启动时检查,之后新增或删除 workflow 文件不会反映出来。
68+
69+
## 不在范围内
70+
71+
- 内置 `deep-research` workflow(第 12 项的另一块剩余部分)。
72+
- 名字来源白名单。
73+
- 会话中途切换锁。
74+
75+
## 验证
76+
77+
- 单元测试覆盖:每一种被拒与被接受的来源;宿主运行(脚本、脚本路径、嵌套路径);schema 与描述形态;恢复名匹配与不匹配两种情况;通知文案;registry 抛错时的关键词提醒;斜杠命令;Config 在启动时确定锁;该键与 `crossSessionInbound` 的收紧基线;ACP 能力位。
78+
- 对每个新分支的手工变异都至少让一个测试失败。

0 commit comments

Comments
 (0)