@akira-tl/forgerelay 1.2.1 → 1.2.3

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,22 @@ All notable ForgeRelay changes are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.2.3] - 2026-09-14
8
+
9
+ ### Fixed
10
+
11
+ - MCP App 静态资源现在在 API Origin validation 之前进入专用静态资源路由,使 ChatGPT `*.web-sandbox.oaiusercontent.com` iframe 可以跨域加载 JavaScript/CSS;MCP、OAuth 与其他 API 路由仍继续拒绝未授权 Origin,不扩大 API 信任边界。
12
+ - 交互式 `7677` debug 实例现在复用生产 ForgeRelay 的 Skill source(默认 `~/.forgerelay/skills`),同时继续隔离 debug 的 config、auth、state 与端口,使 Activity Panel / Workspace Skill 展示与生产发现语义一致。
13
+
14
+ ## [1.2.2] - 2026-09-13
15
+
16
+ ### Fixed
17
+
18
+ - 修复带路径前缀的 `publicBaseUrl` 在 MCP SDK v2 下被 Origin 校验错误拒绝的问题;ForgeRelay 现在只从 canonical public URL 派生允许的 Origin hostname,同时继续拒绝 foreign / malformed Origin。
19
+ - OAuth authorization callback 现在带回 metadata 中公布的 `iss`,使 routed public URL 的现代 OAuth 流程保持 issuer 一致。
20
+ - MCP App 的 CSP `resourceDomains` / `connectDomains` 现在使用 URL Origin,而不是包含部署路径的完整 `publicBaseUrl`,避免 path-prefix 部署下 Activity Panel 只停留在 `Waiting for Activity Panel state.`。
21
+ - Skill 默认发现范围收敛为 Project `.agents/skills`、Project `.forgerelay/skills` 与 active ForgeRelay config `skills`;不再自动扫描全局 `~/.agents/skills` 或 `FORGERELAY_AGENT_DIR/skills`,显式 `FORGERELAY_SKILL_PATHS` 仍作为用户附加来源。
22
+
7
23
  ## [1.2.1] - 2026-09-13
8
24
 
9
25
  ### Fixed
@@ -56,7 +56,7 @@ export function createForgeRelayAuthRouter(options) {
56
56
  res.status(200).json({ ...tokens, ...(instanceId ? { instance_id: instanceId } : {}) });
57
57
  });
58
58
  }
59
- router.use(authorizationPaths, authorizationHandler({ provider }));
59
+ router.use(authorizationPaths, authorizationHandler({ provider, issuerUrl }));
60
60
  router.use(tokenPaths, tokenHandler({ provider }));
61
61
  if (provider.clientsStore.registerClient && registrationEndpoint) {
62
62
  router.use(registrationPaths, clientRegistrationHandler({ clientsStore: provider.clientsStore }));
@@ -64,9 +64,10 @@ function currentIdentity(config, fallbackRevision) {
64
64
  return identity;
65
65
  }
66
66
  function activityPanelCsp(config) {
67
+ const publicOrigins = Array.from(new Set(config.publicBaseUrls.map((baseUrl) => new URL(baseUrl).origin)));
67
68
  return {
68
- resourceDomains: [...config.publicBaseUrls],
69
- connectDomains: [...config.publicBaseUrls],
69
+ resourceDomains: publicOrigins,
70
+ connectDomains: publicOrigins,
70
71
  };
71
72
  }
72
73
  async function readActivityPanelAppResource(config, fallbackRevision, requestedUri, transportSessionId) {
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { checkResourceAllowed, createMcpHandler, isInitializeRequest, isLegacyRequest, resourceUrlFromServerUrl, } from "@modelcontextprotocol/server";
3
- import { createMcpExpressApp, getOAuthProtectedResourceMetadataUrl, requireBearerAuth, } from "@modelcontextprotocol/express";
2
+ import { checkResourceAllowed, createMcpHandler, isInitializeRequest, isLegacyRequest, localhostAllowedOrigins, resourceUrlFromServerUrl, } from "@modelcontextprotocol/server";
3
+ import { getOAuthProtectedResourceMetadataUrl, hostHeaderValidation, localhostHostValidation, originValidation, requireBearerAuth, } from "@modelcontextprotocol/express";
4
4
  import { NodeStreamableHTTPServerTransport, toNodeHandler, toWebRequest, } from "@modelcontextprotocol/node";
5
5
  import express from "express";
6
6
  import { ActivityAuditStore } from "../../../activity/history/audit-store.js";
@@ -32,17 +32,25 @@ const MCP_TRANSPORT_CLEANUP_INTERVAL_MS = 5 * 60 * 1_000;
32
32
  export function createHttpServer(config, options, createMcpServer) {
33
33
  const incomingArtifactAdapters = options.incomingArtifactAdapters
34
34
  ?? [createOpenAIIncomingArtifactAdapter()];
35
+ const routeBaseUrls = config.publicBaseUrls.map((baseUrl) => new URL(baseUrl));
35
36
  const allowedHosts = config.allowedHosts.includes("*")
36
37
  ? undefined
37
38
  : Array.from(new Set([config.host, ...config.allowedHosts]));
38
- const app = createMcpExpressApp({
39
- host: config.host,
40
- ...(allowedHosts ? { allowedHosts } : {}),
41
- });
39
+ const allowedOrigins = Array.from(new Set([
40
+ ...localhostAllowedOrigins(),
41
+ ...routeBaseUrls.map((baseUrl) => baseUrl.hostname),
42
+ ]));
43
+ const app = express();
44
+ app.use(express.json());
45
+ if (allowedHosts) {
46
+ app.use(hostHeaderValidation(allowedHosts));
47
+ }
48
+ else if (["127.0.0.1", "localhost", "::1"].includes(config.host)) {
49
+ app.use(localhostHostValidation());
50
+ }
42
51
  const transports = new McpTransportRegistry({
43
52
  maxTransports: MAX_MCP_TRANSPORT_SESSIONS,
44
53
  });
45
- const routeBaseUrls = config.publicBaseUrls.map((baseUrl) => new URL(baseUrl));
46
54
  const mcpUrl = publicEndpointUrl(config.publicBaseUrl, "mcp");
47
55
  const mcpPaths = publicEndpointPaths(routeBaseUrls, "mcp");
48
56
  const activityPanelAssetsPaths = publicEndpointPaths(routeBaseUrls, "mcp-app-assets");
@@ -188,16 +196,10 @@ export function createHttpServer(config, options, createMcpServer) {
188
196
  });
189
197
  next();
190
198
  });
191
- app.use(createForgeRelayAuthRouter({
192
- provider: oauthProvider,
193
- cliAuthenticationProvider: oauthProvider,
194
- instanceId: config.instanceId,
195
- issuerUrl: new URL(config.publicBaseUrl),
196
- resourceServerUrl,
197
- routeBaseUrls,
198
- scopesSupported: config.oauth.scopes,
199
- resourceName: "ForgeRelay",
200
- }));
199
+ // MCP App assets are intentionally public, cross-origin resources. ChatGPT
200
+ // renders an app on a dedicated `*.web-sandbox.oaiusercontent.com` origin,
201
+ // so these routes must run before MCP/API Origin validation. Host validation
202
+ // still applies above, while MCP, OAuth, and health routes remain protected.
201
203
  app.options(activityPanelAssetsPaths.map((assetPath) => `${assetPath}/{*asset}`), (_req, res) => {
202
204
  setActivityPanelAssetHeaders(res);
203
205
  res.sendStatus(204);
@@ -208,6 +210,17 @@ export function createHttpServer(config, options, createMcpServer) {
208
210
  fallthrough: false,
209
211
  setHeaders: setActivityPanelAssetHeaders,
210
212
  }));
213
+ app.use(originValidation(allowedOrigins));
214
+ app.use(createForgeRelayAuthRouter({
215
+ provider: oauthProvider,
216
+ cliAuthenticationProvider: oauthProvider,
217
+ instanceId: config.instanceId,
218
+ issuerUrl: new URL(config.publicBaseUrl),
219
+ resourceServerUrl,
220
+ routeBaseUrls,
221
+ scopesSupported: config.oauth.scopes,
222
+ resourceName: "ForgeRelay",
223
+ }));
211
224
  app.get(healthPaths, (_req, res) => {
212
225
  res.json({ ok: true, name: "forgerelay" });
213
226
  });
@@ -8,9 +8,8 @@ const FRONTMATTER_DELIMITER = "---";
8
8
  export function effectiveSkillPaths(config, cwd) {
9
9
  const defaultPathCandidates = [
10
10
  resolve(cwd, ".agents", "skills"),
11
- join(homedir(), ".agents", "skills"),
11
+ resolve(cwd, ".forgerelay", "skills"),
12
12
  config.configSkillsDir,
13
- join(config.agentDir, "skills"),
14
13
  ];
15
14
  const defaultPaths = defaultPathCandidates.filter((path) => path !== undefined && existsSync(path));
16
15
  const seen = new Set();
@@ -189,7 +189,8 @@ instructions before the requested file content. Side-effecting file tools and sh
189
189
  commands discover instructions before execution; if new local instructions are
190
190
  found, ForgeRelay returns them and requires the Agent to retry, so the side effect
191
191
  does not occur before the relevant instructions are known. `FORGERELAY_AGENT_DIR`
192
- is not an instruction source; it remains only a compatibility skill-discovery path.
192
+ is neither an instruction source nor an automatic Skill-discovery source; it remains
193
+ an integration runtime directory for supported Agent tooling.
193
194
 
194
195
  ## MCP capability loading
195
196
 
@@ -232,17 +233,16 @@ force the Host to discard a cached tool schema.
232
233
 
233
234
  ## Agent Skills
234
235
 
235
- ForgeRelay discovers standard Agent Skills in precedence order from:
236
+ ForgeRelay discovers Skills in precedence order from:
236
237
 
237
238
  - project `.agents/skills`
238
- - `~/.agents/skills`
239
- - the active ForgeRelay config directory's `skills` folder
240
- - `FORGERELAY_AGENT_DIR/skills` (defaults to `~/.codex/skills`)
241
- - paths from `FORGERELAY_SKILL_PATHS`
239
+ - project `.forgerelay/skills`
240
+ - the active ForgeRelay config directory's `skills` folder (`~/.forgerelay/skills` by default)
241
+ - paths explicitly added through `FORGERELAY_SKILL_PATHS`
242
242
 
243
- These paths are discovery sources, not one shared ownership domain. `.agents/skills` is the open Agent Skills ecosystem and may contain files or symlinks managed by other Agent tooling. ForgeRelay-owned Skills remain private to the active ForgeRelay config directory (`~/.forgerelay/skills` by default) and are never migrated into `~/.agents/skills`.
243
+ These paths are discovery sources, not one shared ownership domain. Project `.agents/skills` is the open Agent Skills ecosystem and may contain files or symlinks managed by other Agent tooling. ForgeRelay-owned project/system Skills stay under `.forgerelay/skills` and the active ForgeRelay config directory. ForgeRelay does not automatically scan global Agent runtime Skill directories such as `~/.agents/skills` or `FORGERELAY_AGENT_DIR/skills`.
244
244
 
245
- Same-named collisions use the first source, so project Skills override global Skills.
245
+ Same-named collisions use the first source: project Agent Skills override project ForgeRelay Skills, which override system ForgeRelay Skills and explicit additional paths.
246
246
 
247
247
  When a task matches an advertised skill, read its `SKILL.md` before using other
248
248
  files in the skill directory.
@@ -910,20 +910,19 @@ select a global instruction file; its `skills` child is an additional Agent Skil
910
910
  | --- | --- |
911
911
  | `FORGERELAY_SKILLS` | Set to `0` to hide skills. Enabled by default. |
912
912
  | `FORGERELAY_SUBAGENTS` | Set to `1` to expose configured subagent profiles. |
913
- | `FORGERELAY_AGENT_DIR` | Defaults to `~/.codex`; its `skills` child is included as an additional Skill source. |
913
+ | `FORGERELAY_AGENT_DIR` | Defaults to `~/.codex`; used by supported Agent integrations, not as an automatic Skill source. |
914
914
  | `FORGERELAY_SKILL_PATHS` | Optional comma-separated additional skill directories. |
915
915
 
916
- Standard Agent Skills are discovered in precedence order from:
916
+ Skills are discovered in precedence order from:
917
917
 
918
918
  - project `.agents/skills`
919
- - `~/.agents/skills`
920
- - the active ForgeRelay config directory's `skills` folder
921
- - `FORGERELAY_AGENT_DIR/skills`
922
- - paths from `FORGERELAY_SKILL_PATHS`
919
+ - project `.forgerelay/skills`
920
+ - the active ForgeRelay config directory's `skills` folder (`~/.forgerelay/skills` by default)
921
+ - paths explicitly added through `FORGERELAY_SKILL_PATHS`
923
922
 
924
- The ownership boundaries are different even though all of these are readable Skill sources. Project/global `.agents/skills` belong to the open Agent Skills ecosystem and may contain files or symlinks installed by other Agent tooling. ForgeRelay-owned Skills stay under the active ForgeRelay config directory (`~/.forgerelay/skills` by default); ForgeRelay does not migrate or install its private Skills into `~/.agents/skills`.
923
+ Project `.agents/skills` belongs to the open Agent Skills ecosystem and may contain files or symlinks installed by other Agent tooling. ForgeRelay-owned project/system Skills stay under `.forgerelay/skills` and the active ForgeRelay config directory. Global Agent runtime directories such as `~/.agents/skills` and `FORGERELAY_AGENT_DIR/skills` are not scanned automatically.
925
924
 
926
- When the same Skill name appears in more than one source, the first source wins. Project Skills therefore override same-named global `~/.agents/skills` entries, matching ForgeRelay's project-over-global configuration model.
925
+ When the same Skill name appears in more than one source, the first source wins: project Agent Skills override project ForgeRelay Skills, which override system ForgeRelay Skills and explicit additional paths.
927
926
 
928
927
  When subagents are enabled, canonical v1.2 profiles are discovered from:
929
928
 
package/docs/gotchas.md CHANGED
@@ -192,15 +192,14 @@ Skills are enabled by default. Check:
192
192
  FORGERELAY_SKILLS=1 forgerelay serve
193
193
  ```
194
194
 
195
- Standard paths include:
195
+ Standard automatic discovery paths include:
196
196
 
197
197
  - project `.agents/skills`
198
- - `~/.agents/skills`
198
+ - project `.forgerelay/skills`
199
199
  - active ForgeRelay config `skills` directory (`~/.forgerelay/skills` by default)
200
- - `FORGERELAY_AGENT_DIR/skills`
201
- - additional `FORGERELAY_SKILL_PATHS`
200
+ - additional paths explicitly configured through `FORGERELAY_SKILL_PATHS`
202
201
 
203
- `.agents/skills` is an external/open Agent Skill source and may contain symlinks managed by other Agent tooling. ForgeRelay-owned Skills belong under its own config `skills` directory and must not be migrated into `.agents/skills`.
202
+ ForgeRelay does **not** automatically scan global Agent runtime Skill directories such as `~/.agents/skills` or `FORGERELAY_AGENT_DIR/skills`. Project `.agents/skills` remains the open Agent Skills source, while ForgeRelay-owned project/system Skills live under `.forgerelay/skills` and the active ForgeRelay config `skills` directory.
204
203
 
205
204
  ## Subagent profiles do not appear
206
205
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akira-tl/forgerelay",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
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",
@@ -67,6 +67,16 @@ export function createInteractiveDebugEnvironment({
67
67
  const configDir = interactiveDebugConfigDir({ env, home });
68
68
  const ownerToken = interactiveDebugOwnerToken(configDir);
69
69
  const { baseUrl, mcpUrl } = interactiveDebugUrls(configDir);
70
+ const productConfigDir = env.FORGERELAY_DEBUG_PRODUCT_CONFIG_DIR
71
+ ? resolve(env.FORGERELAY_DEBUG_PRODUCT_CONFIG_DIR.startsWith("~/")
72
+ ? join(home, env.FORGERELAY_DEBUG_PRODUCT_CONFIG_DIR.slice(2))
73
+ : env.FORGERELAY_DEBUG_PRODUCT_CONFIG_DIR)
74
+ : resolve(join(home, ".forgerelay"));
75
+ const productSkillPaths = [
76
+ join(productConfigDir, "skills"),
77
+ env.FORGERELAY_SKILL_PATHS,
78
+ ].filter((value) => typeof value === "string" && value.trim());
79
+ const productSkillsEnabled = env.FORGERELAY_SKILLS;
70
80
  mkdirSync(debugRoot, { recursive: true });
71
81
 
72
82
  const debugEnv = { ...env };
@@ -84,6 +94,10 @@ export function createInteractiveDebugEnvironment({
84
94
  }
85
95
  }
86
96
  debugEnv.FORGERELAY_CONFIG_DIR = configDir;
97
+ debugEnv.FORGERELAY_SKILL_PATHS = productSkillPaths.join(",");
98
+ if (productSkillsEnabled !== undefined) {
99
+ debugEnv.FORGERELAY_SKILLS = productSkillsEnabled;
100
+ }
87
101
 
88
102
  return { ownerToken, configDir, baseUrl, mcpUrl, env: debugEnv };
89
103
  }
@@ -40,6 +40,8 @@ test("interactive debug uses one dedicated persisted config under ~/.forgerelay/
40
40
  FORGERELAY_ALLOWED_HOSTS: "wrong.example.test",
41
41
  FORGERELAY_OAUTH_OWNER_TOKEN: "wrong-owner-password-123456",
42
42
  FORGERELAY_WIDGETS: "off",
43
+ FORGERELAY_SKILL_PATHS: "/extra/product/skills",
44
+ FORGERELAY_SKILLS: "1",
43
45
  FORGERELAY_LOG_LEVEL: "debug",
44
46
  },
45
47
  home,
@@ -62,6 +64,11 @@ test("interactive debug uses one dedicated persisted config under ~/.forgerelay/
62
64
  assert.equal(result.env.FORGERELAY_ALLOWED_HOSTS, undefined);
63
65
  assert.equal(result.env.FORGERELAY_OAUTH_OWNER_TOKEN, undefined);
64
66
  assert.equal(result.env.FORGERELAY_WIDGETS, undefined);
67
+ assert.equal(
68
+ result.env.FORGERELAY_SKILL_PATHS,
69
+ [join(home, ".forgerelay", "skills"), "/extra/product/skills"].join(","),
70
+ );
71
+ assert.equal(result.env.FORGERELAY_SKILLS, "1");
65
72
  assert.equal(result.env.FORGERELAY_LOG_LEVEL, "debug");
66
73
  });
67
74
 
@@ -80,8 +87,12 @@ test("interactive debug config directory may be explicitly relocated without fal
80
87
  await writeFile(join(customDir, "config.json"), JSON.stringify({ host: "127.0.0.1", port: 6768 }) + "\n");
81
88
  await writeFile(join(customDir, "auth.json"), JSON.stringify({ ownerToken: "custom-debug-password-123456" }) + "\n");
82
89
 
90
+ const productConfigDir = join(home, "custom-product-config");
83
91
  const result = createInteractiveDebugEnvironment({
84
- env: { FORGERELAY_DEBUG_CONFIG_DIR: customDir },
92
+ env: {
93
+ FORGERELAY_DEBUG_CONFIG_DIR: customDir,
94
+ FORGERELAY_DEBUG_PRODUCT_CONFIG_DIR: productConfigDir,
95
+ },
85
96
  home,
86
97
  });
87
98
 
@@ -90,4 +101,5 @@ test("interactive debug config directory may be explicitly relocated without fal
90
101
  assert.equal(result.env.FORGERELAY_CONFIG_DIR, customDir);
91
102
  assert.equal(result.baseUrl, "http://127.0.0.1:6768");
92
103
  assert.equal(result.mcpUrl, "http://127.0.0.1:6768/mcp");
104
+ assert.equal(result.env.FORGERELAY_SKILL_PATHS, join(productConfigDir, "skills"));
93
105
  });