@akira-tl/forgerelay 0.2.4 → 0.2.6

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/docs/roadmap.md CHANGED
@@ -82,6 +82,26 @@ Hooks v1 的目标是给用户和 Agent 一个很小、自动、可组合的生
82
82
 
83
83
  0.2 不引入审批 UI、HTTP/prompt/agent handler、Git 字符串解析器或插件注册表。只有出现真实需求时再扩展 handler 类型。
84
84
 
85
+ ### 0.2.5 — MCP App template reliability
86
+
87
+ 0.2.5 收敛 ChatGPT/MCP App 模板身份与排障链路:
88
+
89
+ - 当前 UI resource URI 由实际构建出的 JavaScript/CSS 内容哈希生成,而不是仅依赖 npm 版本;
90
+ - `ui://forgerelay/workspace-app.html` 继续作为 legacy pointer;
91
+ - 历史 `workspace-app-*.html` URI 通过兼容 resource template 继续读取当前模板,避免旧 metadata snapshot 直接变成 missing resource;
92
+ - debug 日志区分 current / legacy / historical template read,并保留 asset request trace;
93
+ - 7677 acceptance 覆盖 tool metadata、resource list/template list、三类 template read 和静态 bundle HTTP fetch。
94
+
95
+ ### 0.2.6 — identity and transport terminology
96
+
97
+ 0.2.6 整理 ForgeRelay 中多个 `session` 含义,不改变 workspace-first 架构:
98
+
99
+ - `workspaceId` 是唯一持久的逻辑工作身份,跨请求和 transport 重连保持连续;
100
+ - `requestId` 只追踪单次 HTTP/JSON-RPC 请求,不持久化;
101
+ - MCP 协议层 session 在内部和 debug 输出中明确称为 `transportSessionId`,业务状态不得依赖它;
102
+ - 后台命令句柄以 `processId` / process handle 为 canonical 名称,旧 `sessionId` 在 0.2.x 保留 deprecated alias 兼容窗口;
103
+ - 为 stateless MCP transport 做准备,同时保留旧协议兼容 adapter,避免把 transport 生命周期重新提升成 ForgeRelay 会话模型。
104
+
85
105
  ## 0.3 — LSP code intelligence v1
86
106
 
87
107
  LSP is moderate implementation complexity if ForgeRelay does not become a
@@ -90,6 +110,12 @@ language-server installer.
90
110
  The first version should launch only language servers already available on the
91
111
  user's machine or explicitly configured by the user/project.
92
112
 
113
+ During 0.3 development, MCP App UI hardening can land alongside the LSP work when
114
+ it does not distort the code-intelligence scope. In particular, evaluate a more
115
+ self-contained template/bootstrap bundle, reduce avoidable external chunk fetches,
116
+ and keep the current content-hash/compatibility-resource contract intact. This is
117
+ reliability work, not a requirement to move application state into the UI.
118
+
93
119
  Initial operations:
94
120
 
95
121
  - diagnostics;
package/docs/security.md CHANGED
@@ -124,8 +124,9 @@ Do not describe ForgeRelay as a sandboxed coding environment.
124
124
 
125
125
  Shell execution has a 300-second foreground wait ceiling, not a 300-second
126
126
  process lifetime. When `bash` is still running after that window, ForgeRelay
127
- returns a process `sessionId` and leaves the process alive. `write_stdin` can poll,
128
- wait, interact, or explicitly interrupt it. An asynchronously completed process
127
+ returns a canonical `processId` and leaves the process alive. `write_stdin` can
128
+ poll, wait, interact, or explicitly interrupt it. The former process `sessionId`
129
+ remains a deprecated compatibility alias during 0.2.x. An asynchronously completed process
129
130
  is reported on a later tool result for the same logical workspace ID, including
130
131
  error-result paths, and is never broadcast to another workspace ID. Explicitly
131
132
  resuming the same workspace ID in another conversation intentionally transfers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akira-tl/forgerelay",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
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",
@@ -41,7 +41,7 @@
41
41
  "debug:accept": "node scripts/debug/accept.mjs",
42
42
  "postinstall": "node scripts/fix-node-pty-permissions.mjs",
43
43
  "start": "node dist/cli.js serve",
44
- "test": "tsx src/config.test.ts && tsx src/logger.test.ts && tsx src/hooks.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/file-mutations.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
44
+ "test": "tsx src/config.test.ts && tsx src/logger.test.ts && tsx src/mcp-app-template.test.ts && tsx src/hooks.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/file-mutations.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
45
45
  "typecheck": "tsc -p tsconfig.json --noEmit",
46
46
  "release:check": "node scripts/release-version.mjs check",
47
47
  "release:tag-check": "node scripts/release-version.mjs tag",
@@ -38,7 +38,13 @@ await assertDebugPortFree();
38
38
  rmSync(acceptanceRoot, { recursive: true, force: true });
39
39
  mkdirSync(acceptanceRoot, { recursive: true });
40
40
 
41
- const { env } = createDebugEnvironment({ ownerToken, stateDir, worktreeRoot, hookLog });
41
+ const { env } = createDebugEnvironment({
42
+ ownerToken,
43
+ stateDir,
44
+ worktreeRoot,
45
+ hookLog,
46
+ widgets: "full",
47
+ });
42
48
  const server = spawn(process.execPath, ["--import", "tsx", "src/cli.ts", "serve"], {
43
49
  cwd: repoRoot,
44
50
  env,
@@ -120,12 +126,76 @@ try {
120
126
  assert.match(bashTool?.description ?? "", /write_stdin/);
121
127
  const writeStdinTool = tools.find((tool) => tool.name === "write_stdin");
122
128
  assert.equal(writeStdinTool?.inputSchema?.properties?.yieldTimeMs?.maximum, 300000);
129
+ assert.match(
130
+ writeStdinTool?.inputSchema?.properties?.processId?.description ?? "",
131
+ /Canonical process identifier/,
132
+ );
133
+ assert.match(
134
+ writeStdinTool?.inputSchema?.properties?.sessionId?.description ?? "",
135
+ /Deprecated alias for processId/,
136
+ );
123
137
  const openWorkspaceTool = tools.find((tool) => tool.name === "open_workspace");
124
138
  assert.ok(openWorkspaceTool?.inputSchema?.properties?.workspaceId);
125
139
  assert.ok(openWorkspaceTool?.inputSchema?.properties?.newWorkspace);
126
140
  assert.ok(openWorkspaceTool?.outputSchema?.properties?.staleWorkspaces);
141
+ const templateUri = bashTool?._meta?.ui?.resourceUri;
142
+ assert.match(
143
+ templateUri ?? "",
144
+ /^ui:\/\/forgerelay\/workspace-app-[0-9a-f]{12}\.html$/,
145
+ JSON.stringify(bashTool ?? {}),
146
+ );
147
+ assert.deepEqual(bashTool?._meta?.ui?.visibility, ["model", "app"]);
148
+ assert.equal(bashTool?._meta?.["openai/outputTemplate"], templateUri);
127
149
  pass("MCP tools/list", `${toolNames.length} tools: ${toolNames.join(", ")}`);
128
150
 
151
+ const resources = mcpRequest(oauth.accessToken, sessionId, {
152
+ jsonrpc: "2.0",
153
+ id: 21,
154
+ method: "resources/list",
155
+ params: {},
156
+ }).message.result.resources;
157
+ assert.ok(resources.some((resource) => resource.uri === templateUri));
158
+ assert.ok(resources.some((resource) => resource.uri === "ui://forgerelay/workspace-app.html"));
159
+
160
+ const resourceTemplates = mcpRequest(oauth.accessToken, sessionId, {
161
+ jsonrpc: "2.0",
162
+ id: 22,
163
+ method: "resources/templates/list",
164
+ params: {},
165
+ }).message.result.resourceTemplates;
166
+ assert.ok(resourceTemplates.some(
167
+ (resourceTemplate) => resourceTemplate.uriTemplate === "ui://forgerelay/workspace-app-{revision}.html",
168
+ ));
169
+
170
+ const readTemplate = (id, uri) => mcpRequest(oauth.accessToken, sessionId, {
171
+ jsonrpc: "2.0",
172
+ id,
173
+ method: "resources/read",
174
+ params: { uri },
175
+ }).message.result.contents[0];
176
+
177
+ const template = readTemplate(23, templateUri);
178
+ assert.equal(template.uri, templateUri);
179
+ assert.equal(template.mimeType, "text/html;profile=mcp-app");
180
+ assert.match(template.text ?? "", /<script type="module" crossorigin src="[^"]+\/mcp-app-assets\//);
181
+ assert.ok(template._meta?.ui?.csp?.resourceDomains?.includes(debugBaseUrl));
182
+ const scriptUrl = template.text?.match(/<script type="module" crossorigin src="([^"]+)"/)?.[1];
183
+ assert.ok(scriptUrl);
184
+ const scriptAsset = curlRequest({ method: "GET", url: scriptUrl });
185
+ assert.equal(scriptAsset.status, 200, scriptAsset.body);
186
+
187
+ const legacyTemplate = readTemplate(24, "ui://forgerelay/workspace-app.html");
188
+ assert.equal(legacyTemplate.uri, "ui://forgerelay/workspace-app.html");
189
+ assert.equal(legacyTemplate.mimeType, "text/html;profile=mcp-app");
190
+ assert.equal(legacyTemplate.text, template.text);
191
+
192
+ const historicalUri = "ui://forgerelay/workspace-app-0.2.4.html";
193
+ const historicalTemplate = readTemplate(25, historicalUri);
194
+ assert.equal(historicalTemplate.uri, historicalUri);
195
+ assert.equal(historicalTemplate.mimeType, "text/html;profile=mcp-app");
196
+ assert.equal(historicalTemplate.text, template.text);
197
+ pass("MCP app template", `${templateUri} + legacy/history compatibility -> ${scriptUrl}`);
198
+
129
199
  const opened = callTool(oauth.accessToken, sessionId, 3, "open_workspace", {
130
200
  path: checkoutWorkspace,
131
201
  });
@@ -155,7 +225,7 @@ try {
155
225
  });
156
226
  assert.match(shell.structuredContent.result, /debug-bash-ok/);
157
227
  assert.equal(shell.structuredContent.running, false);
158
- pass("bash", "foreground command completed through ProcessSessionManager");
228
+ pass("bash", "foreground command completed through ProcessManager");
159
229
 
160
230
  const failedEdit = callTool(oauth.accessToken, sessionId, 7, "edit", {
161
231
  workspaceId,
@@ -364,7 +434,13 @@ function initializeRequest(id) {
364
434
  method: "initialize",
365
435
  params: {
366
436
  protocolVersion: "2025-06-18",
367
- capabilities: {},
437
+ capabilities: {
438
+ extensions: {
439
+ "io.modelcontextprotocol/ui": {
440
+ mimeTypes: ["text/html;profile=mcp-app"],
441
+ },
442
+ },
443
+ },
368
444
  clientInfo: { name: "forgerelay-debug-acceptance", version: "1.0.0" },
369
445
  },
370
446
  };
@@ -20,6 +20,7 @@ export function createDebugEnvironment({
20
20
  stateDir = resolve(debugRoot, "state"),
21
21
  worktreeRoot = resolve(debugRoot, "worktrees"),
22
22
  hookLog = debugHookLog,
23
+ widgets = process.env.FORGERELAY_DEBUG_WIDGETS ?? "off",
23
24
  } = {}) {
24
25
  const token = ownerToken ?? process.env.FORGERELAY_DEBUG_OWNER_TOKEN ?? createDebugOwnerToken();
25
26
  mkdirSync(debugRoot, { recursive: true });
@@ -37,7 +38,7 @@ export function createDebugEnvironment({
37
38
  FORGERELAY_WORKTREE_ROOT: worktreeRoot,
38
39
  FORGERELAY_OAUTH_OWNER_TOKEN: token,
39
40
  FORGERELAY_TOOL_MODE: "full",
40
- FORGERELAY_WIDGETS: "off",
41
+ FORGERELAY_WIDGETS: widgets,
41
42
  FORGERELAY_LOG_LEVEL: process.env.FORGERELAY_LOG_LEVEL ?? "info",
42
43
  FORGERELAY_LOG_FORMAT: process.env.FORGERELAY_LOG_FORMAT ?? "pretty",
43
44
  FORGERELAY_DEBUG_HOOK_RECORDER: debugHookRecorder,