@akira-tl/forgerelay 1.2.4 → 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 CHANGED
@@ -4,6 +4,18 @@ 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
+
7
19
  ## [1.2.4] - 2026-09-15
8
20
 
9
21
  ### Added
package/README.md CHANGED
@@ -64,7 +64,7 @@ forgerelay config sources
64
64
  forgerelay config explain <logical-path>
65
65
  ```
66
66
 
67
- v1.2 的 Config System 统一解析 User、Project 和机器私有 Project Local 配置,统一优先级是 `runtime > project-local > project > user > built-in`。External MCP、Language Servers、Hooks 和 Subagent Profiles 支持按需热刷新与 last-known-good;需要重启的 General 配置会区分 configured / applied,不会自动重启 Server。Canonical JSON 可以引用 npm package 中的 `schemas/v1/*` `$schema`,受支持的敏感字段可用 `${ENV_NAME}`,诊断不会输出解析后的 secretForgeRelay 启动不会自动重写旧配置,需要迁移时使用 `forgerelay config migrate --dry-run ...` 先预览。
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;集成回主分支时只接受安全的 fast-forward,不自动制造 merge conflict。
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。v1.2.0 已把项目中的可执行配置统一接入 Project execution trust seam,但首个稳定策略是 compatibility-allow;完整的交互式 Project Trust approval UI 不是 v1.2.0 已实现功能。
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 v2 in v1.2 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; restart-required General settings keep configured and applied values distinct and never auto-restart the server. Canonical JSON can point at the packaged `schemas/v1/*` `$schema` files, supported sensitive fields may use `${ENV_NAME}`, and diagnostics do not expose resolved secrets. Startup never rewrites legacy configuration automatically; preview an explicit migration with `forgerelay config migrate --dry-run ...` first.
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, with fast-forward-only finalization.
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. v1.2.0 routes executable project configuration through a shared Project execution trust seam, but its initial policy is compatibility-allow. A full interactive Project Trust approval UI is not claimed for v1.2.0.
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.
@@ -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
- writes.push(jsonWrite(files.configPath, nextConfig));
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 Number.isInteger(port) && port >= 1 && port <= 65535
122
+ return isIntegerPort(port, LISTEN_PORT_MIN, PORT_MAX)
122
123
  ? undefined
123
- : "Enter a port between 1 and 65535.";
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();
@@ -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(1).max(65535), "Local listening port.", {
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", 1, 65535)),
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("Detected launcher shell with the recorded compatibility fallback used when needed."),
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(1024).max(65535),
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 (!Number.isInteger(callbackPort) || Number(callbackPort) < 1024 || Number(callbackPort) > 65535) {
140
- throw new Error(`${label}.callbackPort must be an integer from 1024 to 65535.`);
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, config, 0o600);
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akira-tl/forgerelay",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
4
4
  "description": "Local development control plane for MCP coding agents.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/Akira-TL/forgerelay#readme",
@@ -26,7 +26,7 @@
26
26
  }
27
27
  },
28
28
  "port": {
29
- "description": "Local listening port.",
29
+ "description": "Local listening port (1-65535).",
30
30
  "type": "integer",
31
31
  "minimum": 1,
32
32
  "maximum": 65535,
@@ -431,7 +431,7 @@
431
431
  "x-forgerelay-sensitivity": "public",
432
432
  "x-forgerelay-interpolation": "none",
433
433
  "x-forgerelay-execution-effect": "none",
434
- "x-forgerelay-computed-default": "Detected launcher shell with the recorded compatibility fallback used when needed."
434
+ "x-forgerelay-computed-default": "Platform compatibility default unless an explicit shell preference is recorded."
435
435
  },
436
436
  "shellInstructions": {
437
437
  "description": "Enable ForgeRelay-owned runtime shell instructions.",