@akira-tl/forgerelay 1.2.3 → 1.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +19 -11
- package/capabilities/workspace/managed-worktrees/GUIDE.md +4 -2
- package/dist/cli/config/migrate.js +5 -2
- package/dist/cli/init/setup-config.js +0 -2
- package/dist/cli/setup-support.js +3 -2
- package/dist/mcp/server/core/schemas.js +6 -0
- package/dist/mcp/server/workspace/runtime/workspace-open-presentation.js +4 -2
- package/dist/mcp/server/workspace/runtime/workspace-open-schema.js +6 -1
- package/dist/mcp/server/workspace/runtime/workspace-open.js +19 -13
- package/dist/runtime/config/definition/general-config.js +4 -3
- package/dist/runtime/config/external-mcp-config.js +4 -3
- package/dist/runtime/config/user-config.js +4 -1
- package/dist/runtime/config/validation/ports.js +9 -0
- package/dist/runtime/logging/logger.js +19 -19
- package/dist/ui/.vite/manifest.json +276 -276
- package/dist/ui/activity-panel-app.html +2 -2
- package/dist/ui/assets/activity-panel-app-Dq2xLIrs.css +1 -0
- package/dist/ui/assets/{activity-panel-app-BeDiwd7D.js → activity-panel-app-hkM8rX3z.js} +1 -1
- package/dist/ui/assets/{angular-html-DuoCE7nn.js → angular-html-UD0GEFFl.js} +1 -1
- package/dist/ui/assets/{angular-ts-D5tGk0hL.js → angular-ts-B2awh-Q1.js} +1 -1
- package/dist/ui/assets/{apl-lciOTPbw.js → apl-CpbhnJc8.js} +1 -1
- package/dist/ui/assets/{astro-vJbqfNnJ.js → astro-CkVVuF5B.js} +1 -1
- package/dist/ui/assets/{blade-C7655Q4K.js → blade-czG58aop.js} +1 -1
- package/dist/ui/assets/{c-DdHp1Jrw.js → c-DSq6h06b.js} +1 -1
- package/dist/ui/assets/{cobol-DgvzQxZP.js → cobol-CYnmhT2E.js} +1 -1
- package/dist/ui/assets/{coffee-BIjVUOUd.js → coffee-ClLhNqd5.js} +1 -1
- package/dist/ui/assets/{cpp-CU8tGzvI.js → cpp-BZDC6U43.js} +1 -1
- package/dist/ui/assets/{crystal-CUQAwsg9.js → crystal-BN9jCJyU.js} +1 -1
- package/dist/ui/assets/{css-DtMTqD3F.js → css-8dMfiihi.js} +1 -1
- package/dist/ui/assets/{edge-DoCX9RnL.js → edge-DemPBRNz.js} +1 -1
- package/dist/ui/assets/{elixir-DJxV9_U6.js → elixir-bLdHW8dz.js} +1 -1
- package/dist/ui/assets/{elm-DIgWqK4r.js → elm-DlZPcXmu.js} +1 -1
- package/dist/ui/assets/{erb-BHxP0c_e.js → erb-wmTqbHwm.js} +1 -1
- package/dist/ui/assets/{git-rebase-HbI_Vo8d.js → git-rebase-BqrZJVYO.js} +1 -1
- package/dist/ui/assets/{glimmer-js-CthY4Ty_.js → glimmer-js-CkPzdNHI.js} +1 -1
- package/dist/ui/assets/{glimmer-ts-B2-_A1Xz.js → glimmer-ts-Bd0vTdJC.js} +1 -1
- package/dist/ui/assets/{glsl-D9V9gq98.js → glsl-DKK0Kibz.js} +1 -1
- package/dist/ui/assets/{graphql-CddeAaS3.js → graphql-DmFr8ZvB.js} +1 -1
- package/dist/ui/assets/{hack-D_loGkOT.js → hack-BQcuk9lx.js} +1 -1
- package/dist/ui/assets/{haml-BoWDczd0.js → haml-Dbx0cG5B.js} +1 -1
- package/dist/ui/assets/{handlebars-ZoSOvw9y.js → handlebars-D0nD8yLi.js} +1 -1
- package/dist/ui/assets/{heavy-payload-DO34LwoZ.js → heavy-payload-CPOM8G8z.js} +3 -3
- package/dist/ui/assets/{html-CE-obXVo.js → html-9xkoIci1.js} +1 -1
- package/dist/ui/assets/{html-derivative-Cd-smAbW.js → html-derivative-Cg7n4YW0.js} +1 -1
- package/dist/ui/assets/{http-CZrkbrSl.js → http-Ckt_4_pP.js} +1 -1
- package/dist/ui/assets/{hurl-CxTuxBt1.js → hurl-BxMgqn9u.js} +1 -1
- package/dist/ui/assets/{java-BshLjCat.js → java-DxijgO-5.js} +1 -1
- package/dist/ui/assets/{javascript-D2pxQTiX.js → javascript-CusLzlRb.js} +1 -1
- package/dist/ui/assets/{jinja-kbfPP7-F.js → jinja-DBTRfImq.js} +1 -1
- package/dist/ui/assets/{jison-C2mt9l1M.js → jison-oCbVte0g.js} +1 -1
- package/dist/ui/assets/{json-DU_rg_AU.js → json-uq2yfoO2.js} +1 -1
- package/dist/ui/assets/{jsx-D_8aQMlx.js → jsx-B3AfJWyQ.js} +1 -1
- package/dist/ui/assets/{julia-BmefTaVV.js → julia-BFvq-UTm.js} +1 -1
- package/dist/ui/assets/{just-DXV8vk1S.js → just-Ck0W9Rcj.js} +1 -1
- package/dist/ui/assets/{latex-Cjm2MvGW.js → latex-BjKhTmaO.js} +1 -1
- package/dist/ui/assets/{liquid-DyQDlo0u.js → liquid-CsaE0q4b.js} +1 -1
- package/dist/ui/assets/{lua-B2A1vzzy.js → lua-D0MSBhv0.js} +1 -1
- package/dist/ui/assets/{marko-B9vOkXOU.js → marko-CTLxSPPr.js} +1 -1
- package/dist/ui/assets/{mdc-D42m6spU.js → mdc-BwDM_Bct.js} +1 -1
- package/dist/ui/assets/{nginx-TpfVxSUY.js → nginx-C5Q5rNaB.js} +1 -1
- package/dist/ui/assets/{nim-CJr5T1FK.js → nim-BjKHRt6k.js} +1 -1
- package/dist/ui/assets/{perl-cJZcsdm9.js → perl-BrABfniL.js} +1 -1
- package/dist/ui/assets/{php-CSZL5KRT.js → php-BWY__Khq.js} +1 -1
- package/dist/ui/assets/{pug-B1eMKzuA.js → pug-BD4PlwIK.js} +1 -1
- package/dist/ui/assets/{qml-Dl2_7zc9.js → qml-yQtdZNrK.js} +1 -1
- package/dist/ui/assets/{r-F0aaO9gG.js → r-D0tlixrf.js} +1 -1
- package/dist/ui/assets/{razor-BPnkSMCw.js → razor-CsmyuKJs.js} +1 -1
- package/dist/ui/assets/{regexp-BnGHMFbv.js → regexp-BXU4cTCD.js} +1 -1
- package/dist/ui/assets/{rst-B3CO7WYO.js → rst-CGlzAlrM.js} +1 -1
- package/dist/ui/assets/{ruby-B6VMzT1T.js → ruby-CRKDbfJj.js} +1 -1
- package/dist/ui/assets/{sas-Df2lHVuU.js → sas-CKDY6iPG.js} +1 -1
- package/dist/ui/assets/{scss-4XxI6sgu.js → scss-BCoXLSqD.js} +1 -1
- package/dist/ui/assets/{shellscript-DYASCN4r.js → shellscript-BVuRs_tF.js} +1 -1
- package/dist/ui/assets/{shellsession-Br6AGJO_.js → shellsession-alj1b7vN.js} +1 -1
- package/dist/ui/assets/{soy-p3ZNq1We.js → soy-BbQDrw9i.js} +1 -1
- package/dist/ui/assets/{sql-CAdv2OXL.js → sql-BdcqHaEA.js} +1 -1
- package/dist/ui/assets/{stata-Dx7yUbG_.js → stata-5IC5m_XB.js} +1 -1
- package/dist/ui/assets/{surrealql-6YsE8HQa.js → surrealql-BfVFLYzV.js} +1 -1
- package/dist/ui/assets/{svelte-YgzedwpD.js → svelte-B22aFi25.js} +1 -1
- package/dist/ui/assets/{templ-Bahqx3uD.js → templ-BRo7TcBX.js} +1 -1
- package/dist/ui/assets/{tex-o6fTQjkH.js → tex-CYhw-JbD.js} +1 -1
- package/dist/ui/assets/{ts-tags-BO-Co0Ju.js → ts-tags-C56aGOzq.js} +1 -1
- package/dist/ui/assets/{tsx-DtQe5AoH.js → tsx-IkFyQoro.js} +1 -1
- package/dist/ui/assets/{twig-BcUs6Kqc.js → twig-NkXaxf7l.js} +1 -1
- package/dist/ui/assets/{typescript-BPtM8h4u.js → typescript-AgbZakqy.js} +1 -1
- package/dist/ui/assets/{vue-5KkGUzJJ.js → vue-Cxz1Rk-f.js} +1 -1
- package/dist/ui/assets/{vue-html-Df_Xfsz9.js → vue-html-DWHLzP-w.js} +1 -1
- package/dist/ui/assets/{vue-vine-hUIe_u_Y.js → vue-vine-WR64TKte.js} +1 -1
- package/dist/ui/assets/{xml-C3Jy7as5.js → xml-CNdUn7jo.js} +1 -1
- package/dist/ui/assets/{xsl-BnF9OuFo.js → xsl-DLRRUZ98.js} +1 -1
- package/dist/ui/assets/{yaml-DyrFgCou.js → yaml-gltXViA8.js} +1 -1
- package/dist/workspace-colors.js +54 -0
- package/dist/workspaces/git/git-worktrees.js +53 -14
- package/dist/workspaces/inventory.js +2 -0
- package/dist/workspaces/presentation/workspace-presentation.js +13 -0
- package/dist/workspaces/relay/workspace-relay.js +3 -0
- package/dist/workspaces/sessions.js +28 -8
- package/dist/workspaces.js +9 -9
- package/docs/chatgpt-coding-workflow.md +16 -8
- package/docs/gotchas.md +2 -2
- package/package.json +2 -1
- package/schemas/v1/config.user.schema.json +2 -2
- package/scripts/ci/cmd-acceptance.mjs +3 -85
- package/scripts/ci/gates/acceptance-runtime.mjs +45 -0
- package/scripts/ci/gates/platform-tests.mjs +27 -17
- package/scripts/ci/gates/prepare-windows-shell-runtime.mjs +45 -0
- package/scripts/ci/gates/test-shard.mjs +29 -7
- package/scripts/ci/powershell51-acceptance.mjs +10 -91
- package/scripts/ci/pwsh-acceptance.mjs +7 -87
- package/scripts/ci/windows-product-acceptance.mjs +55 -0
- package/scripts/release/release-gate.test.mjs +121 -13
- package/scripts/release-parity.mjs +12 -1
- package/dist/ui/assets/activity-panel-app-DkfJzTZ3.css +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,33 @@ All notable ForgeRelay changes are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.2.5] - 2026-09-16
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Config v2 现在把监听端口的支持范围统一为 `1`–`65535`,并让 JSON Schema、环境变量、`init --advanced`、持久化写入、迁移与运行时使用同一验证约束;External MCP OAuth `callbackPort` 继续保持独立的 `1024`–`65535` 范围。
|
|
12
|
+
- 初始化只持久化用户真正选择的网络 override;HTTPS proxy 场景不再重复写入可从 loopback bind 与 public URL 安全推导的 `trustedProxies: ["loopback"]`。
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- `config.json` 写入与 legacy migration 现在会在持久化前通过 Config v2 schema 校验,避免 CLI 或迁移先写出运行时随后拒绝的非法配置;迁移遇到非法 General Config 时会在创建 canonical target 或 backup 之前失败。
|
|
17
|
+
- 明确 `host` / `port`、`publicBaseUrl`、`allowedHosts` 与 `trustedProxies` 的配置权威关系:显式用户值始终优先,派生值只在对应 override 缺失时生效。
|
|
18
|
+
|
|
19
|
+
## [1.2.4] - 2026-09-15
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- Managed worktrees can now start from arbitrary Git commit-ish refs while keeping their integration target branch independent, so a pinned historical baseline no longer has to be a local branch.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Release verification now splits more independent CI work in parallel and reuses pinned npm / packaged shell runtime state where safe, reducing repeated setup in the cross-platform release matrix.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- Pinned-base managed worktrees now preserve their baseline metadata across reuse, reopen, recovery, rebase, and close, while still integrating into the configured target branch; pinned workspaces with different immutable baselines no longer collapse onto one reuse identity.
|
|
32
|
+
- Workspace Panel long paths now wrap instead of truncating important Root / checkout details, and Workspace identity color is shared consistently between pretty logs and Panel SVG icons. The identity palette now uses 14 normal/bright terminal color slots, avoids duplicates for the first 14 distinct Workspace names in one process, and normalizes both POSIX and Windows-style paths consistently.
|
|
33
|
+
|
|
7
34
|
## [1.2.3] - 2026-09-14
|
|
8
35
|
|
|
9
36
|
### Fixed
|
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ forgerelay config sources
|
|
|
64
64
|
forgerelay config explain <logical-path>
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
ForgeRelay 的 Config System 统一解析 User、Project 和机器私有 Project Local 配置,优先级是 `runtime > project-local > project > user > built-in`。External MCP、Language Servers、Hooks 和 Subagent Profiles 支持按需刷新与 last-known-good;启动不会自动重写旧配置。需要深入配置、迁移或排查来源时,直接看 [配置指南](https://github.com/Akira-TL/forgerelay/wiki/Configuration)。
|
|
68
68
|
|
|
69
69
|
### ChatGPT 访问不到 localhost?
|
|
70
70
|
|
|
@@ -100,9 +100,9 @@ bash(command="npm test")
|
|
|
100
100
|
bash(command="git status --short")
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
-
ForgeRelay 不会默认为每个任务创建 worktree。只有你明确要求隔离或并行开发时,才使用 managed worktree。
|
|
103
|
+
ForgeRelay 不会默认为每个任务创建 worktree。只有你明确要求隔离或并行开发时,才使用 managed worktree。Worktree 可以从本地 branch、tag 或历史 commit SHA 启动,同时把最终集成的本地 `targetBranch` 单独记录;不需要为了历史基线先造一个临时 branch。
|
|
104
104
|
|
|
105
|
-
长命令也不会要求 Agent 高频轮询。命令超过当前等待窗口时会返回稳定的 `processId
|
|
105
|
+
长命令也不会要求 Agent 高频轮询。命令超过当前等待窗口时会返回稳定的 `processId`,后续继续等待、交互或中断同一个进程即可。
|
|
106
106
|
|
|
107
107
|
## 主要能力
|
|
108
108
|
|
|
@@ -110,8 +110,9 @@ ForgeRelay 不会默认为每个任务创建 worktree。只有你明确要求隔
|
|
|
110
110
|
- Linux / macOS 可以用 Bash、zsh、POSIX sh;Windows 原生支持 PowerShell 7、Windows PowerShell 5.1 和 `cmd.exe`。
|
|
111
111
|
- 同一个 checkout 会保留自己的 Workspace 身份。换一次对话,不需要重新创建工作区。
|
|
112
112
|
- `code.intelligence` 可以查 definition、hover、references、symbols 和 diagnostics。
|
|
113
|
-
- 项目里的 `AGENTS.md`、`CLAUDE.md` 和 Agent Skills 按需加载,不会每次都把整套说明重新塞进上下文。
|
|
114
|
-
- 需要并行开发时可以创建真实 Git worktree
|
|
113
|
+
- 项目里的 `AGENTS.md`、`CLAUDE.md` 和 Agent Skills 按需加载,不会每次都把整套说明重新塞进上下文。Skill 来自项目级 `.agents/skills`、项目/系统 ForgeRelay Skills 和显式附加路径;不会自动扫描全局 `~/.agents/skills`。
|
|
114
|
+
- 需要并行开发时可以创建真实 Git worktree;起始 commit 与最终 `targetBranch` 独立,集成仍只接受安全的 fast-forward,不自动制造 merge conflict。
|
|
115
|
+
- 支持 MCP Apps 的 Host 会显示 Workspace Summary / Activity Panel;Workspace 身份色在 Panel 与 pretty log 中保持一致,长路径会在详情区正常换行。
|
|
115
116
|
- Workspace Relay 可以把执行放到另一台 ForgeRelay;Composite Workspace 可以同时协调几个独立环境。
|
|
116
117
|
- `read` 可以直接把 PNG、JPEG、WebP 和 GIF 作为临时 MCP Media content 返回;外部 MCP 通过独立的 global/Project `mcp.json` 热加载到 `mcp.external` Capability,并支持人工 OAuth 认证,而不会自动打开它返回的路径或 URL。
|
|
117
118
|
|
|
@@ -125,7 +126,7 @@ ForgeRelay 给 Agent 的是真实本机执行权限,不是模拟环境。
|
|
|
125
126
|
|
|
126
127
|
文件工具受 Workspace 和 allowed roots 限制;Shell 命令使用启动 ForgeRelay 的本地用户权限执行,**Shell 不是 OS sandbox**。因此只连接你信任的 MCP Host,只开放确实需要的项目目录,并保护好 Owner password。
|
|
127
128
|
|
|
128
|
-
Allowed roots 是文件系统 / Workspace authority,不等于 Project Trust approval
|
|
129
|
+
Allowed roots 是文件系统 / Workspace authority,不等于 Project Trust approval。项目中的可执行配置已经统一接入 Project execution trust seam;当前稳定策略仍是 compatibility-allow,完整的交互式 Project Trust approval UI 尚未实现。
|
|
129
130
|
|
|
130
131
|
ForgeRelay 默认拒绝 elevated / administrator 启动。只有你显式选择高权限运行时才会继续,并会提示系统级修改可能不可逆。
|
|
131
132
|
|
|
@@ -135,6 +136,8 @@ ForgeRelay 默认拒绝 elevated / administrator 启动。只有你显式选择
|
|
|
135
136
|
|
|
136
137
|
- [快速开始](https://github.com/Akira-TL/forgerelay/wiki/Getting-Started)
|
|
137
138
|
- [配置](https://github.com/Akira-TL/forgerelay/wiki/Configuration)
|
|
139
|
+
- [Managed Worktree](https://github.com/Akira-TL/forgerelay/wiki/Managed-Worktrees)
|
|
140
|
+
- [ChatGPT 与 MCP 工作流](https://github.com/Akira-TL/forgerelay/wiki/ChatGPT-and-MCP-Workflow)
|
|
138
141
|
- [External MCP](https://github.com/Akira-TL/forgerelay/wiki/External-MCP)
|
|
139
142
|
- [安全模型](https://github.com/Akira-TL/forgerelay/wiki/Security)
|
|
140
143
|
- [故障排查](https://github.com/Akira-TL/forgerelay/wiki/Troubleshooting)
|
|
@@ -185,7 +188,7 @@ forgerelay config sources
|
|
|
185
188
|
forgerelay config explain <logical-path>
|
|
186
189
|
```
|
|
187
190
|
|
|
188
|
-
Config System
|
|
191
|
+
ForgeRelay's Config System resolves User, Project, and machine-private Project Local sources using `runtime > project-local > project > user > built-in`. External MCP, Language Servers, Hooks, and Subagent Profiles refresh on demand with last-known-good protection, and startup never rewrites legacy configuration automatically. See [Configuration](https://github.com/Akira-TL/forgerelay/wiki/Configuration) for scopes, migration, schemas, and diagnostics.
|
|
189
192
|
|
|
190
193
|
### Host cannot reach localhost?
|
|
191
194
|
|
|
@@ -221,7 +224,7 @@ bash(command="npm test")
|
|
|
221
224
|
bash(command="git status --short")
|
|
222
225
|
```
|
|
223
226
|
|
|
224
|
-
ForgeRelay does not create a worktree for every task. Managed worktrees are for cases where you explicitly want isolation or parallel development.
|
|
227
|
+
ForgeRelay does not create a worktree for every task. Managed worktrees are for cases where you explicitly want isolation or parallel development. A worktree may start from a local branch, tag, or historical commit SHA while keeping the eventual local `targetBranch` separate, so a temporary baseline branch is unnecessary.
|
|
225
228
|
|
|
226
229
|
Long commands do not require tight polling either. Once the current wait window expires, ForgeRelay returns a stable `processId`; later calls wait on, interact with, or interrupt that same process.
|
|
227
230
|
|
|
@@ -231,8 +234,9 @@ Long commands do not require tight polling either. Once the current wait window
|
|
|
231
234
|
- Linux/macOS can use Bash, zsh, or POSIX sh. Windows has native PowerShell 7, Windows PowerShell 5.1, and `cmd.exe` support.
|
|
232
235
|
- Reopening the same checkout reuses the same Workspace identity instead of creating another one for every conversation.
|
|
233
236
|
- `code.intelligence` provides definition, hover, references, symbols, and diagnostics.
|
|
234
|
-
- `AGENTS.md`, `CLAUDE.md`, and Agent Skills are loaded as needed instead of being resent in full on every open.
|
|
235
|
-
- Managed worktrees provide real Git isolation when you ask for parallel work,
|
|
237
|
+
- `AGENTS.md`, `CLAUDE.md`, and Agent Skills are loaded as needed instead of being resent in full on every open. Skills come from project `.agents/skills`, project/system ForgeRelay Skill directories, and explicit extra paths; global `~/.agents/skills` is not scanned automatically.
|
|
238
|
+
- Managed worktrees provide real Git isolation when you ask for parallel work. Starting commit and local `targetBranch` are tracked independently, while finalization remains fast-forward-only.
|
|
239
|
+
- MCP Apps-capable hosts can render the Workspace Summary / Activity Panel. Workspace identity colors stay consistent between the panel and pretty logs, and long paths wrap in detail rows instead of hiding their tail.
|
|
236
240
|
- Workspace Relay runs work on another ForgeRelay instance; Composite Workspaces coordinate several independent environments from one Host.
|
|
237
241
|
- `read` can return PNG, JPEG, WebP, and GIF directly as transient MCP Media content; external MCP servers hot-reload from standalone global/Project `mcp.json` files into the `mcp.external` Capability, with explicit human OAuth when needed and no automatic dereferencing of returned paths or URLs.
|
|
238
242
|
|
|
@@ -246,7 +250,7 @@ ForgeRelay gives an Agent real local execution capability.
|
|
|
246
250
|
|
|
247
251
|
Filesystem tools are constrained by the opened Workspace and configured allowed roots. Shell commands run with the authority of the local user running ForgeRelay; **the shell is not an OS sandbox**. Connect only MCP hosts you trust, expose only project roots you want an Agent to access, and keep the Owner password private.
|
|
248
252
|
|
|
249
|
-
Allowed roots grant filesystem/Workspace authority; they are not a Project Trust approval.
|
|
253
|
+
Allowed roots grant filesystem/Workspace authority; they are not a Project Trust approval. Executable project configuration is routed through a shared Project execution trust seam; the current stable policy remains compatibility-allow, and a full interactive Project Trust approval UI is not implemented yet.
|
|
250
254
|
|
|
251
255
|
Elevated / administrator startup is rejected by default. It only proceeds after explicit opt-in, with a warning that system-level AI-driven changes may be irreversible.
|
|
252
256
|
|
|
@@ -256,6 +260,8 @@ See the [Security model](https://github.com/Akira-TL/forgerelay/wiki/Security) f
|
|
|
256
260
|
|
|
257
261
|
- [Getting Started](https://github.com/Akira-TL/forgerelay/wiki/Getting-Started)
|
|
258
262
|
- [Configuration](https://github.com/Akira-TL/forgerelay/wiki/Configuration)
|
|
263
|
+
- [Managed Worktrees](https://github.com/Akira-TL/forgerelay/wiki/Managed-Worktrees)
|
|
264
|
+
- [ChatGPT and MCP Workflow](https://github.com/Akira-TL/forgerelay/wiki/ChatGPT-and-MCP-Workflow)
|
|
259
265
|
- [External MCP](https://github.com/Akira-TL/forgerelay/wiki/External-MCP)
|
|
260
266
|
- [Security model](https://github.com/Akira-TL/forgerelay/wiki/Security)
|
|
261
267
|
- [Troubleshooting](https://github.com/Akira-TL/forgerelay/wiki/Troubleshooting)
|
|
@@ -276,3 +282,5 @@ npm run build
|
|
|
276
282
|
```
|
|
277
283
|
|
|
278
284
|
`npm run dev` uses the 7677 debug runtime. Product port 7676 is kept separate from development acceptance.
|
|
285
|
+
|
|
286
|
+
Release tags are created only after a developer has manually exercised and explicitly accepted the exact release-ready state. Automated verification and the Linux/macOS/Windows release matrix remain additional gates; they do not replace manual acceptance.
|
|
@@ -15,8 +15,10 @@
|
|
|
15
15
|
|
|
16
16
|
- 默认 `mode="checkout"`,直接使用用户已有 checkout;
|
|
17
17
|
- 只有明确需要隔离/并行 Git 工作时才选 `mode="worktree"`;
|
|
18
|
-
- `baseRef` 只用于 managed worktree
|
|
19
|
-
-
|
|
18
|
+
- `baseRef` 只用于 managed worktree,表示新 managed branch 的起始 commit-ish;可以是本地 branch、tag、commit SHA 等可解析到 commit 的 ref,默认使用 source checkout 当前 `HEAD`;
|
|
19
|
+
- `targetBranch` 表示最终允许 `close_workspace` fast-forward 的本地 branch;未显式指定时,本地 branch `baseRef` 自身优先作为 target,否则使用 source checkout 当前本地 branch;detached source 无法推导 target 时必须显式提供;
|
|
20
|
+
- 显式固定到历史 base 的请求按解析后的 `baseSha` 区分复用身份,不会与同一 target 上另一历史 base 或普通 branch-following backing 混用;
|
|
21
|
+
- 已有 `workspaceId` 用于恢复同一个逻辑 workspace;closed managed-worktree Workspace reopen 时从当前持久化 `targetBranch` 建立新 backing,不重新钉回最初的历史 `baseRef`;
|
|
20
22
|
- `newWorkspace` 只创建新的逻辑 handle;
|
|
21
23
|
- `newWorktree` 才表示同一项目再创建一个独立物理 worktree。
|
|
22
24
|
|
|
@@ -2,6 +2,7 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
2
2
|
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { basename, join, resolve } from "node:path";
|
|
4
4
|
import { parse as parseYaml } from "yaml";
|
|
5
|
+
import { parseConfigSource } from "../../runtime/config/definition/definition.js";
|
|
5
6
|
import { configSchemaId } from "../../runtime/config/definition/schema.js";
|
|
6
7
|
import { generalConfigDefinition } from "../../runtime/config/definition/general-config.js";
|
|
7
8
|
import { externalMcpConfigDefinition } from "../../runtime/config/definition/external-mcp.js";
|
|
@@ -137,8 +138,10 @@ function buildGlobalMigrationPlan(configDir) {
|
|
|
137
138
|
nextConfig.$schema = generalSchema;
|
|
138
139
|
configChanged = true;
|
|
139
140
|
}
|
|
140
|
-
if (configChanged)
|
|
141
|
-
|
|
141
|
+
if (configChanged) {
|
|
142
|
+
const validatedConfig = parseConfigSource(generalConfigDefinition, "user", nextConfig);
|
|
143
|
+
writes.push(jsonWrite(files.configPath, validatedConfig));
|
|
144
|
+
}
|
|
142
145
|
if (configChanged && files.configExists)
|
|
143
146
|
backupSources.push({ path: files.configPath, relativePath: "config.json" });
|
|
144
147
|
if (files.hooksExists)
|
|
@@ -21,8 +21,6 @@ function applyNetworkSelection(config, selection) {
|
|
|
21
21
|
}
|
|
22
22
|
config.publicBaseUrl = selection.publicBaseUrl;
|
|
23
23
|
}
|
|
24
|
-
if (selection.mode === "proxy")
|
|
25
|
-
config.trustedProxies = ["loopback"];
|
|
26
24
|
}
|
|
27
25
|
function applyAdvancedSelection(config, selection) {
|
|
28
26
|
if (selection.port === 7676)
|
|
@@ -2,6 +2,7 @@ import { isIP } from "node:net";
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import * as prompts from "@clack/prompts";
|
|
4
4
|
import { satisfies } from "semver";
|
|
5
|
+
import { isIntegerPort, LISTEN_PORT_MIN, PORT_MAX } from "../runtime/config/validation/ports.js";
|
|
5
6
|
const SUPPORTED_NODE_RANGE = ">=20.12 <27";
|
|
6
7
|
const require = createRequire(import.meta.url);
|
|
7
8
|
export function isNullConfigValue(value) {
|
|
@@ -118,9 +119,9 @@ export async function textPrompt(options) {
|
|
|
118
119
|
}
|
|
119
120
|
export function validatePort(value) {
|
|
120
121
|
const port = Number(value);
|
|
121
|
-
return
|
|
122
|
+
return isIntegerPort(port, LISTEN_PORT_MIN, PORT_MAX)
|
|
122
123
|
? undefined
|
|
123
|
-
:
|
|
124
|
+
: `Enter a port between ${LISTEN_PORT_MIN} and ${PORT_MAX}.`;
|
|
124
125
|
}
|
|
125
126
|
export function isLoopbackBindAddress(value) {
|
|
126
127
|
const host = value.trim().toLowerCase();
|
|
@@ -122,6 +122,8 @@ export const workspaceInventoryEntryOutputSchema = z.object({
|
|
|
122
122
|
state: z.enum(["active", "stale", "invalid", "closed"]),
|
|
123
123
|
mode: z.enum(["checkout", "worktree"]),
|
|
124
124
|
sourceRoot: z.string().optional(),
|
|
125
|
+
baseRef: z.string().optional(),
|
|
126
|
+
baseSha: z.string().optional(),
|
|
125
127
|
branch: z.string().optional(),
|
|
126
128
|
targetBranch: z.string().optional(),
|
|
127
129
|
managed: z.boolean(),
|
|
@@ -181,6 +183,8 @@ export const workspaceInspectionOutputSchema = z.union([
|
|
|
181
183
|
state: z.enum(["active", "stale", "invalid", "closed"]),
|
|
182
184
|
mode: z.enum(["checkout", "worktree"]),
|
|
183
185
|
sourceRoot: z.string().optional(),
|
|
186
|
+
baseRef: z.string().optional(),
|
|
187
|
+
baseSha: z.string().optional(),
|
|
184
188
|
branch: z.string().optional(),
|
|
185
189
|
targetBranch: z.string().optional(),
|
|
186
190
|
managed: z.boolean(),
|
|
@@ -201,6 +205,8 @@ export const workspaceInspectionOutputSchema = z.union([
|
|
|
201
205
|
state: z.enum(["active", "stale", "invalid", "closed"]).optional(),
|
|
202
206
|
mode: z.enum(["checkout", "worktree"]),
|
|
203
207
|
sourceRoot: z.string().optional(),
|
|
208
|
+
baseRef: z.string().optional(),
|
|
209
|
+
baseSha: z.string().optional(),
|
|
204
210
|
branch: z.string().optional(),
|
|
205
211
|
targetBranch: z.string().optional(),
|
|
206
212
|
managed: z.boolean().optional(),
|
|
@@ -10,13 +10,14 @@ import { capabilityContextFor } from "../../core/capability-support.js";
|
|
|
10
10
|
import { redactSkillDiagnosticPaths } from "../../core/schemas.js";
|
|
11
11
|
import { logToolCall, workspaceLogContext } from "../../core/tool-support.js";
|
|
12
12
|
import { buildExecutionShellContext } from "../../../server-instructions.js";
|
|
13
|
+
import { workspaceColorFor, workspaceIdentityName } from "../../../../workspace-colors.js";
|
|
13
14
|
export const workspaceTaskUsageInstruction = "Use workspace.tasks proactively for work that spans multiple steps or sessions: create or update Tasks to preserve useful next steps and current state, and read its summary when resuming relevant unfinished work. Do not query it mechanically on every open_workspace call.";
|
|
14
15
|
export const capabilityDiscoveryInstruction = "For capability describe/run, use only names present in capabilityCatalog. capabilityFingerprint.capabilities is a semantic feature fingerprint, not a callable registry; capabilityGuides are documentation descriptors, not callable capabilities.";
|
|
15
16
|
export async function presentLocalWorkspaceOpen(options, input, contextData) {
|
|
16
17
|
const { config, forgerelayVersion: FORGERELAY_VERSION, workspaces, workspaceTasks, reviewCheckpoints, capabilityRegistry, subagentProviders, hooks, rememberWorkspacePanelState, } = options;
|
|
17
|
-
const { path, workspaceId, mode, baseRef, newWorktree, newWorkspace, context } = input;
|
|
18
|
+
const { path, workspaceId, mode, baseRef, targetBranch, newWorktree, newWorkspace, context } = input;
|
|
18
19
|
const { conversationScopeId, protectedWorkspaceIds, startedAt, sessionId } = contextData;
|
|
19
|
-
const { workspace, agentsFiles, availableAgentsFiles, hookReports, workspaceReused, bootstrapContextComponents, contextFingerprint, } = await workspaces.openWorkspace({ path, workspaceId, mode, baseRef, newWorktree, newWorkspace, context }, {
|
|
20
|
+
const { workspace, agentsFiles, availableAgentsFiles, hookReports, workspaceReused, bootstrapContextComponents, contextFingerprint, } = await workspaces.openWorkspace({ path, workspaceId, mode, baseRef, targetBranch, newWorktree, newWorkspace, context }, {
|
|
20
21
|
conversationScopeId,
|
|
21
22
|
protectedWorkspaceIds,
|
|
22
23
|
});
|
|
@@ -189,6 +190,7 @@ export async function presentLocalWorkspaceOpen(options, input, contextData) {
|
|
|
189
190
|
kind: "workspace",
|
|
190
191
|
root: workspace.root,
|
|
191
192
|
path: workspace.root,
|
|
193
|
+
workspaceColor: workspaceColorFor(workspaceIdentityName(workspace.root)),
|
|
192
194
|
mode: workspace.mode,
|
|
193
195
|
workspaceReused,
|
|
194
196
|
includeBootstrapContext: effectiveIncludeBootstrapContext,
|
|
@@ -21,6 +21,7 @@ export const openWorkspaceToolDefinition = {
|
|
|
21
21
|
relay: z.string().optional().describe("Optional registered remote ForgeRelay alias for a path-backed member."),
|
|
22
22
|
mode: z.enum(["checkout", "worktree"]).optional(),
|
|
23
23
|
baseRef: z.string().optional(),
|
|
24
|
+
targetBranch: z.string().optional(),
|
|
24
25
|
newWorktree: z.boolean().optional(),
|
|
25
26
|
newWorkspace: z.boolean().optional(),
|
|
26
27
|
}).optional().describe("Composite member definition used by action=member."),
|
|
@@ -55,7 +56,11 @@ export const openWorkspaceToolDefinition = {
|
|
|
55
56
|
baseRef: z
|
|
56
57
|
.string()
|
|
57
58
|
.optional()
|
|
58
|
-
.describe("
|
|
59
|
+
.describe("Commit-ish to base a managed worktree on. Only used with mode=\"worktree\"; may be a local branch, tag, commit SHA, or other ref that resolves to a commit. Defaults to HEAD."),
|
|
60
|
+
targetBranch: z
|
|
61
|
+
.string()
|
|
62
|
+
.optional()
|
|
63
|
+
.describe("Existing local branch that close_workspace may fast-forward when finalizing the managed worktree. Only used with mode=\"worktree\". Defaults to baseRef when baseRef names a local branch, otherwise the source checkout's current local branch."),
|
|
59
64
|
newWorktree: z
|
|
60
65
|
.boolean()
|
|
61
66
|
.optional()
|
|
@@ -4,6 +4,7 @@ import { formatPathForPrompt } from "../../../../workspaces/resources/skills.js"
|
|
|
4
4
|
import { compositeCapabilityContext } from "../../core/capability-support.js";
|
|
5
5
|
import { compositeWorkspaceLogContext, logToolCall, textBlock } from "../../core/tool-support.js";
|
|
6
6
|
import { openWorkspaceToolDefinition } from "./workspace-open-schema.js";
|
|
7
|
+
import { workspaceColorFor, workspaceIdentityName } from "../../../../workspace-colors.js";
|
|
7
8
|
import { presentLocalWorkspaceOpen, workspaceTaskUsageInstruction, } from "./workspace-open-presentation.js";
|
|
8
9
|
export function registerOpenWorkspaceTool(options) {
|
|
9
10
|
const { server } = options;
|
|
@@ -11,7 +12,7 @@ export function registerOpenWorkspaceTool(options) {
|
|
|
11
12
|
}
|
|
12
13
|
async function handleOpenWorkspace(options, input, requestContext) {
|
|
13
14
|
const { config, workspaces, remoteWorkspaces, compositeWorkspaces, workspaceTasks, processSessions, capabilityRegistry, compositeTaskGuides, loadCompositeMemberContext, rememberWorkspacePanelState, hostScopeIdFor, contextDeliveryScopeIdFor, presentation, } = options;
|
|
14
|
-
const { action = "open", memberAction, member, kind, name, memberName, path, relay, workspaceId, mode, baseRef, newWorktree, newWorkspace, context, root, status, state, staleOnly, offset, limit, } = input;
|
|
15
|
+
const { action = "open", memberAction, member, kind, name, memberName, path, relay, workspaceId, mode, baseRef, targetBranch, newWorktree, newWorkspace, context, root, status, state, staleOnly, offset, limit, } = input;
|
|
15
16
|
const startedAt = performance.now();
|
|
16
17
|
const conversationScopeId = contextDeliveryScopeIdFor(requestContext);
|
|
17
18
|
const protectedWorkspaceIds = processSessions.activeWorkspaceIds();
|
|
@@ -82,7 +83,7 @@ async function handleOpenWorkspace(options, input, requestContext) {
|
|
|
82
83
|
}
|
|
83
84
|
if (memberAction !== undefined || member !== undefined || kind !== undefined || name !== undefined ||
|
|
84
85
|
memberName !== undefined || path !== undefined || relay !== undefined || mode !== undefined ||
|
|
85
|
-
baseRef !== undefined || newWorktree !== undefined || newWorkspace !== undefined || context !== undefined ||
|
|
86
|
+
baseRef !== undefined || targetBranch !== undefined || newWorktree !== undefined || newWorkspace !== undefined || context !== undefined ||
|
|
86
87
|
root !== undefined || status !== undefined || state !== undefined || staleOnly !== undefined ||
|
|
87
88
|
offset !== undefined || limit !== undefined) {
|
|
88
89
|
throw new Error("open_workspace action=inspect accepts only workspaceId. It never opens, resumes, binds, or mutates the inspected Workspace.");
|
|
@@ -156,7 +157,7 @@ async function handleOpenWorkspace(options, input, requestContext) {
|
|
|
156
157
|
throw new Error("open_workspace action=member requires memberAction and member.");
|
|
157
158
|
}
|
|
158
159
|
if (kind !== undefined || name !== undefined || memberName !== undefined || path !== undefined || relay !== undefined || mode !== undefined ||
|
|
159
|
-
baseRef !== undefined || newWorktree !== undefined || newWorkspace !== undefined || context !== undefined ||
|
|
160
|
+
baseRef !== undefined || targetBranch !== undefined || newWorktree !== undefined || newWorkspace !== undefined || context !== undefined ||
|
|
160
161
|
root !== undefined || status !== undefined || state !== undefined || staleOnly !== undefined ||
|
|
161
162
|
offset !== undefined || limit !== undefined) {
|
|
162
163
|
throw new Error("open_workspace action=member accepts workspaceId, memberAction, and member only. Put any Workspace open definition inside member.");
|
|
@@ -173,8 +174,8 @@ async function handleOpenWorkspace(options, input, requestContext) {
|
|
|
173
174
|
throw new Error("A Composite Workspace cannot be mounted as a Composite Workspace member.");
|
|
174
175
|
}
|
|
175
176
|
if (member.relay !== undefined || member.mode !== undefined || member.baseRef !== undefined ||
|
|
176
|
-
member.newWorktree !== undefined || member.newWorkspace !== undefined) {
|
|
177
|
-
throw new Error("member.workspaceId cannot be combined with relay/mode/baseRef/newWorktree/newWorkspace.");
|
|
177
|
+
member.targetBranch !== undefined || member.newWorktree !== undefined || member.newWorkspace !== undefined) {
|
|
178
|
+
throw new Error("member.workspaceId cannot be combined with relay/mode/baseRef/targetBranch/newWorktree/newWorkspace.");
|
|
178
179
|
}
|
|
179
180
|
if (!remoteWorkspaces.has(targetWorkspaceId))
|
|
180
181
|
workspaces.getWorkspace(targetWorkspaceId);
|
|
@@ -185,6 +186,7 @@ async function handleOpenWorkspace(options, input, requestContext) {
|
|
|
185
186
|
path: member.path,
|
|
186
187
|
...(member.mode ? { mode: member.mode } : {}),
|
|
187
188
|
...(member.baseRef ? { baseRef: member.baseRef } : {}),
|
|
189
|
+
...(member.targetBranch ? { targetBranch: member.targetBranch } : {}),
|
|
188
190
|
...(member.newWorktree !== undefined ? { newWorktree: member.newWorktree } : {}),
|
|
189
191
|
...(member.newWorkspace !== undefined ? { newWorkspace: member.newWorkspace } : {}),
|
|
190
192
|
context: "none",
|
|
@@ -195,6 +197,7 @@ async function handleOpenWorkspace(options, input, requestContext) {
|
|
|
195
197
|
path: member.path,
|
|
196
198
|
...(member.mode ? { mode: member.mode } : {}),
|
|
197
199
|
...(member.baseRef ? { baseRef: member.baseRef } : {}),
|
|
200
|
+
...(member.targetBranch ? { targetBranch: member.targetBranch } : {}),
|
|
198
201
|
...(member.newWorktree !== undefined ? { newWorktree: member.newWorktree } : {}),
|
|
199
202
|
...(member.newWorkspace !== undefined ? { newWorkspace: member.newWorkspace } : {}),
|
|
200
203
|
context: "none",
|
|
@@ -218,8 +221,8 @@ async function handleOpenWorkspace(options, input, requestContext) {
|
|
|
218
221
|
}
|
|
219
222
|
else if (memberAction === "update") {
|
|
220
223
|
const targetFieldsPresent = member.workspaceId !== undefined || member.path !== undefined || member.relay !== undefined ||
|
|
221
|
-
member.mode !== undefined || member.baseRef !== undefined || member.
|
|
222
|
-
member.newWorkspace !== undefined;
|
|
224
|
+
member.mode !== undefined || member.baseRef !== undefined || member.targetBranch !== undefined ||
|
|
225
|
+
member.newWorktree !== undefined || member.newWorkspace !== undefined;
|
|
223
226
|
if (member.newName === undefined && member.purpose === undefined && !targetFieldsPresent) {
|
|
224
227
|
throw new Error("Updating a Composite Workspace member requires newName, purpose, or a replacement Workspace target.");
|
|
225
228
|
}
|
|
@@ -235,7 +238,7 @@ async function handleOpenWorkspace(options, input, requestContext) {
|
|
|
235
238
|
else {
|
|
236
239
|
if (member.newName !== undefined || member.purpose !== undefined || member.workspaceId !== undefined || member.path !== undefined ||
|
|
237
240
|
member.relay !== undefined || member.mode !== undefined || member.baseRef !== undefined ||
|
|
238
|
-
member.newWorktree !== undefined || member.newWorkspace !== undefined) {
|
|
241
|
+
member.targetBranch !== undefined || member.newWorktree !== undefined || member.newWorkspace !== undefined) {
|
|
239
242
|
throw new Error("Removing a Composite Workspace member accepts only member.name.");
|
|
240
243
|
}
|
|
241
244
|
composite = compositeWorkspaces.removeMember(workspaceId, member.name);
|
|
@@ -291,9 +294,9 @@ async function handleOpenWorkspace(options, input, requestContext) {
|
|
|
291
294
|
return response;
|
|
292
295
|
}
|
|
293
296
|
if (action === "list") {
|
|
294
|
-
if (path !== undefined || relay !== undefined || name !== undefined || memberName !== undefined || baseRef !== undefined ||
|
|
295
|
-
newWorkspace !== undefined || context !== undefined) {
|
|
296
|
-
throw new Error("open_workspace action=list does not accept path, relay, name, memberName, baseRef, newWorktree, newWorkspace, or context. Use kind/root/workspaceId/mode/status/state/staleOnly for inventory filters.");
|
|
297
|
+
if (path !== undefined || relay !== undefined || name !== undefined || memberName !== undefined || baseRef !== undefined || targetBranch !== undefined ||
|
|
298
|
+
newWorktree !== undefined || newWorkspace !== undefined || context !== undefined) {
|
|
299
|
+
throw new Error("open_workspace action=list does not accept path, relay, name, memberName, baseRef, targetBranch, newWorktree, newWorkspace, or context. Use kind/root/workspaceId/mode/status/state/staleOnly for inventory filters.");
|
|
297
300
|
}
|
|
298
301
|
const compositeInventory = () => compositeWorkspaces.list()
|
|
299
302
|
.filter((entry) => workspaceId === undefined || entry.id === workspaceId)
|
|
@@ -382,7 +385,7 @@ async function handleOpenWorkspace(options, input, requestContext) {
|
|
|
382
385
|
const openingComposite = kind === "composite" ||
|
|
383
386
|
(workspaceId !== undefined && compositeWorkspaces.has(workspaceId));
|
|
384
387
|
if (openingComposite) {
|
|
385
|
-
if (relay !== undefined || path !== undefined || mode !== undefined || baseRef !== undefined ||
|
|
388
|
+
if (relay !== undefined || path !== undefined || mode !== undefined || baseRef !== undefined || targetBranch !== undefined ||
|
|
386
389
|
newWorktree !== undefined || newWorkspace !== undefined) {
|
|
387
390
|
throw new Error("Composite Workspace open accepts name/workspaceId/context only; members are attached separately and keep their own Workspace definitions.");
|
|
388
391
|
}
|
|
@@ -432,6 +435,7 @@ async function handleOpenWorkspace(options, input, requestContext) {
|
|
|
432
435
|
kind: "composite",
|
|
433
436
|
name: composite.name,
|
|
434
437
|
path: composite.name,
|
|
438
|
+
workspaceColor: workspaceColorFor(workspaceIdentityName(composite.name)),
|
|
435
439
|
members: composite.members,
|
|
436
440
|
instruction,
|
|
437
441
|
summary: { members: composite.members.length, status: composite.status },
|
|
@@ -464,7 +468,7 @@ async function handleOpenWorkspace(options, input, requestContext) {
|
|
|
464
468
|
throw new Error("open_workspace name/memberName are only valid for a Composite Workspace.");
|
|
465
469
|
}
|
|
466
470
|
if (workspaceId !== undefined && remoteWorkspaces.has(workspaceId)) {
|
|
467
|
-
if (path !== undefined || relay !== undefined || mode !== undefined || baseRef !== undefined ||
|
|
471
|
+
if (path !== undefined || relay !== undefined || mode !== undefined || baseRef !== undefined || targetBranch !== undefined ||
|
|
468
472
|
newWorktree !== undefined || newWorkspace !== undefined) {
|
|
469
473
|
throw new Error("Resuming a relayed Workspace by workspaceId accepts context only.");
|
|
470
474
|
}
|
|
@@ -479,6 +483,7 @@ async function handleOpenWorkspace(options, input, requestContext) {
|
|
|
479
483
|
path,
|
|
480
484
|
mode,
|
|
481
485
|
baseRef,
|
|
486
|
+
targetBranch,
|
|
482
487
|
newWorktree,
|
|
483
488
|
newWorkspace,
|
|
484
489
|
context,
|
|
@@ -511,6 +516,7 @@ async function handleOpenWorkspace(options, input, requestContext) {
|
|
|
511
516
|
kind: "workspace",
|
|
512
517
|
root: opened.root,
|
|
513
518
|
path: opened.root,
|
|
519
|
+
workspaceColor: workspaceColorFor(workspaceIdentityName(opened.root)),
|
|
514
520
|
mode: opened.mode,
|
|
515
521
|
relay,
|
|
516
522
|
instruction: opened.instruction,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as z from "zod/v4";
|
|
2
2
|
import { defineConfigDomain } from "./definition.js";
|
|
3
|
+
import { LISTEN_PORT_MIN, PORT_MAX } from "../validation/ports.js";
|
|
3
4
|
const USER_RUNTIME_SCOPES = ["runtime", "user", "built-in"];
|
|
4
5
|
const USER_SCOPES = ["user", "built-in"];
|
|
5
6
|
const USER_ONLY_SCOPES = ["user"];
|
|
@@ -24,11 +25,11 @@ export const generalConfigDefinition = defineConfigDomain({
|
|
|
24
25
|
reload: "restart-required",
|
|
25
26
|
runtimeOverride: runtimeEnv("HOST", (env) => env.HOST),
|
|
26
27
|
}),
|
|
27
|
-
port: field(z.number().int().min(
|
|
28
|
+
port: field(z.number().int().min(LISTEN_PORT_MIN).max(PORT_MAX), `Local listening port (${LISTEN_PORT_MIN}-${PORT_MAX}).`, {
|
|
28
29
|
scopes: USER_RUNTIME_SCOPES,
|
|
29
30
|
builtIn: literal(7676),
|
|
30
31
|
reload: "restart-required",
|
|
31
|
-
runtimeOverride: runtimeEnv("PORT", (env) => readIntegerEnv(env, "PORT",
|
|
32
|
+
runtimeOverride: runtimeEnv("PORT", (env) => readIntegerEnv(env, "PORT", LISTEN_PORT_MIN, PORT_MAX)),
|
|
32
33
|
}),
|
|
33
34
|
allowedRoots: field(z.array(z.string()), "Filesystem roots that Workspaces may open.", {
|
|
34
35
|
scopes: USER_RUNTIME_SCOPES,
|
|
@@ -119,7 +120,7 @@ export const generalConfigDefinition = defineConfigDomain({
|
|
|
119
120
|
}),
|
|
120
121
|
commandShell: field(commandShellSchema, "Recorded command-shell preference for ForgeRelay command and Hook execution.", {
|
|
121
122
|
scopes: USER_SCOPES,
|
|
122
|
-
builtIn: computed("
|
|
123
|
+
builtIn: computed("Platform compatibility default unless an explicit shell preference is recorded."),
|
|
123
124
|
reload: "restart-required",
|
|
124
125
|
}),
|
|
125
126
|
shellInstructions: field(z.boolean(), "Enable ForgeRelay-owned runtime shell instructions.", {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as z from "zod/v4";
|
|
2
|
+
import { isIntegerPort, OAUTH_CALLBACK_PORT_MIN, PORT_MAX } from "./validation/ports.js";
|
|
2
3
|
const MCP_SERVER_NAME_PATTERN = /^[a-z][a-z0-9._-]{0,63}$/;
|
|
3
4
|
const MAX_MCP_SERVERS = 32;
|
|
4
5
|
const nonEmptyStringSchema = z.string().trim().min(1);
|
|
@@ -13,7 +14,7 @@ const clientMetadataUrlSchema = z.string().url().refine((value) => {
|
|
|
13
14
|
}, "clientMetadataUrl must use https and contain a non-root path.");
|
|
14
15
|
const oauthSourceSchema = z.object({
|
|
15
16
|
clientMetadataUrl: clientMetadataUrlSchema,
|
|
16
|
-
callbackPort: z.number().int().min(
|
|
17
|
+
callbackPort: z.number().int().min(OAUTH_CALLBACK_PORT_MIN).max(PORT_MAX),
|
|
17
18
|
}).strict();
|
|
18
19
|
const stdioSourceSchema = z.object({
|
|
19
20
|
transport: z.literal("stdio"),
|
|
@@ -136,8 +137,8 @@ function parseOAuthClientConfig(value, label) {
|
|
|
136
137
|
throw new Error(`${label}.clientMetadataUrl must use https and contain a non-root path.`);
|
|
137
138
|
}
|
|
138
139
|
const callbackPort = value.callbackPort;
|
|
139
|
-
if (!
|
|
140
|
-
throw new Error(`${label}.callbackPort must be an integer from
|
|
140
|
+
if (!isIntegerPort(callbackPort, OAUTH_CALLBACK_PORT_MIN, PORT_MAX)) {
|
|
141
|
+
throw new Error(`${label}.callbackPort must be an integer from ${OAUTH_CALLBACK_PORT_MIN} to ${PORT_MAX}.`);
|
|
141
142
|
}
|
|
142
143
|
return {
|
|
143
144
|
clientMetadataUrl: parsed.toString(),
|
|
@@ -5,6 +5,8 @@ import { homedir } from "node:os";
|
|
|
5
5
|
import { join, resolve } from "node:path";
|
|
6
6
|
import { withFileLock } from "../state/lock/file-lock.js";
|
|
7
7
|
import { expandHomePath } from "../../mcp/filesystem/roots.js";
|
|
8
|
+
import { parseConfigSource } from "./definition/definition.js";
|
|
9
|
+
import { generalConfigDefinition } from "./definition/general-config.js";
|
|
8
10
|
export function forgerelayConfigDir(env = process.env) {
|
|
9
11
|
const explicit = env.FORGERELAY_CONFIG_DIR;
|
|
10
12
|
if (explicit)
|
|
@@ -49,8 +51,9 @@ export function loadForgeRelayFiles(env = process.env, options = {}) {
|
|
|
49
51
|
}
|
|
50
52
|
export function writeForgeRelayConfig(config, env = process.env) {
|
|
51
53
|
const filePath = forgerelayConfigPath(env);
|
|
54
|
+
const validated = parseConfigSource(generalConfigDefinition, "user", config);
|
|
52
55
|
mkdirSync(forgerelayConfigDir(env), { recursive: true });
|
|
53
|
-
writeJsonFile(filePath,
|
|
56
|
+
writeJsonFile(filePath, validated, 0o600);
|
|
54
57
|
return filePath;
|
|
55
58
|
}
|
|
56
59
|
export async function writeForgeRelayAuth(auth, env = process.env) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const LISTEN_PORT_MIN = 1;
|
|
2
|
+
export const PORT_MAX = 65_535;
|
|
3
|
+
export const OAUTH_CALLBACK_PORT_MIN = 1_024;
|
|
4
|
+
export function isIntegerPort(value, minimum = LISTEN_PORT_MIN, maximum = PORT_MAX) {
|
|
5
|
+
return typeof value === "number"
|
|
6
|
+
&& Number.isInteger(value)
|
|
7
|
+
&& value >= minimum
|
|
8
|
+
&& value <= maximum;
|
|
9
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { basename } from "node:path";
|
|
2
1
|
import { styleText } from "node:util";
|
|
2
|
+
import { workspaceColorFor, workspaceIdentityName, } from "../../workspace-colors.js";
|
|
3
3
|
const LEVEL_WEIGHT = {
|
|
4
4
|
silent: 0,
|
|
5
5
|
error: 1,
|
|
@@ -13,14 +13,22 @@ const LEVEL_STYLE = {
|
|
|
13
13
|
info: "green",
|
|
14
14
|
debug: "gray",
|
|
15
15
|
};
|
|
16
|
-
const WORKSPACE_PROJECT_COLORS =
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
|
|
16
|
+
const WORKSPACE_PROJECT_COLORS = {
|
|
17
|
+
red: "red",
|
|
18
|
+
green: "green",
|
|
19
|
+
yellow: "yellow",
|
|
20
|
+
blue: "blue",
|
|
21
|
+
magenta: "magenta",
|
|
22
|
+
cyan: "cyan",
|
|
23
|
+
white: "white",
|
|
24
|
+
redBright: "redBright",
|
|
25
|
+
greenBright: "greenBright",
|
|
26
|
+
yellowBright: "yellowBright",
|
|
27
|
+
blueBright: "blueBright",
|
|
28
|
+
magentaBright: "magentaBright",
|
|
29
|
+
cyanBright: "cyanBright",
|
|
30
|
+
whiteBright: "whiteBright",
|
|
31
|
+
};
|
|
24
32
|
export function shouldLog(config, level) {
|
|
25
33
|
return LEVEL_WEIGHT[config.level] >= LEVEL_WEIGHT[level];
|
|
26
34
|
}
|
|
@@ -59,7 +67,7 @@ export function transportSessionIdPrefix(transportSessionId) {
|
|
|
59
67
|
/** @deprecated Use transportSessionIdPrefix. */
|
|
60
68
|
export const sessionIdPrefix = transportSessionIdPrefix;
|
|
61
69
|
export function workspaceLogLabel(root, workspaceId) {
|
|
62
|
-
return `${
|
|
70
|
+
return `${workspaceIdentityName(root)}/${workspaceId}`;
|
|
63
71
|
}
|
|
64
72
|
export function commandPreview(command) {
|
|
65
73
|
const normalized = command.replace(/\s+/g, " ").trim();
|
|
@@ -215,17 +223,9 @@ function formatPrettySource(source, options) {
|
|
|
215
223
|
}
|
|
216
224
|
const project = source.slice(0, separator);
|
|
217
225
|
const workspace = source.slice(separator + 1);
|
|
218
|
-
const projectColor = WORKSPACE_PROJECT_COLORS[
|
|
226
|
+
const projectColor = WORKSPACE_PROJECT_COLORS[workspaceColorFor(project)];
|
|
219
227
|
return `${style([projectColor, "bold"], project, options)}/${style(["cyan", "underline"], workspace, options)}`;
|
|
220
228
|
}
|
|
221
|
-
function stableColorIndex(value) {
|
|
222
|
-
let hash = 2166136261;
|
|
223
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
224
|
-
hash ^= value.charCodeAt(index);
|
|
225
|
-
hash = Math.imul(hash, 16777619);
|
|
226
|
-
}
|
|
227
|
-
return (hash >>> 0) % WORKSPACE_PROJECT_COLORS.length;
|
|
228
|
-
}
|
|
229
229
|
function formatRuntimeResources(entry) {
|
|
230
230
|
const rssMb = bytesToMegabytes(numberField(entry.rssBytes));
|
|
231
231
|
const heapUsedMb = bytesToMegabytes(numberField(entry.heapUsedBytes));
|