@akira-tl/forgerelay 0.2.3 → 0.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 +24 -0
- package/dist/hooks.js +18 -4
- package/dist/logger.js +56 -2
- package/dist/mcp/server-instructions.js +10 -8
- package/dist/mcp-app-template.js +45 -0
- package/dist/pi-tools.js +1 -9
- package/dist/process-sessions.js +56 -8
- package/dist/roots.js +1 -1
- package/dist/server.js +324 -175
- package/dist/workspace-store.js +13 -0
- package/dist/workspaces.js +275 -25
- package/docs/chatgpt-coding-workflow.md +18 -1
- package/docs/configuration.md +25 -4
- package/docs/debugging.md +35 -7
- package/docs/gotchas.md +22 -1
- package/docs/roadmap.md +26 -0
- package/docs/security.md +10 -0
- package/package.json +2 -2
- package/scripts/debug/accept.mjs +82 -4
- package/scripts/debug/runtime.mjs +2 -1
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;若公开 schema 改名需要兼容窗口,则在明确的版本边界完成;
|
|
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
|
@@ -122,6 +122,16 @@ The security model is therefore based on:
|
|
|
122
122
|
|
|
123
123
|
Do not describe ForgeRelay as a sandboxed coding environment.
|
|
124
124
|
|
|
125
|
+
Shell execution has a 300-second foreground wait ceiling, not a 300-second
|
|
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
|
|
129
|
+
is reported on a later tool result for the same logical workspace ID, including
|
|
130
|
+
error-result paths, and is never broadcast to another workspace ID. Explicitly
|
|
131
|
+
resuming the same workspace ID in another conversation intentionally transfers
|
|
132
|
+
that completion scope as well. Hook handlers keep their separate bounded timeout
|
|
133
|
+
policy because they are lifecycle gates rather than user-command execution.
|
|
134
|
+
|
|
125
135
|
## Lifecycle hooks
|
|
126
136
|
|
|
127
137
|
Hook command 是本地代码执行,使用与 ForgeRelay 相同的操作系统用户权限并继承进程环境。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akira-tl/forgerelay",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.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",
|
|
@@ -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",
|
package/scripts/debug/accept.mjs
CHANGED
|
@@ -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({
|
|
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,
|
|
@@ -108,15 +114,80 @@ try {
|
|
|
108
114
|
params: {},
|
|
109
115
|
}).message.result.tools;
|
|
110
116
|
const toolNames = tools.map((tool) => tool.name);
|
|
111
|
-
for (const expected of ["open_workspace", "close_worktree", "read", "write", "edit", "rename", "delete", "grep", "glob", "ls", "bash"]) {
|
|
117
|
+
for (const expected of ["open_workspace", "close_workspace", "close_worktree", "read", "write", "edit", "rename", "delete", "grep", "glob", "ls", "bash", "write_stdin"]) {
|
|
112
118
|
assert.ok(toolNames.includes(expected), `missing debug tool ${expected}`);
|
|
113
119
|
}
|
|
114
120
|
const bashTool = tools.find((tool) => tool.name === "bash");
|
|
115
121
|
assert.match(bashTool?.description ?? "", /may modify ordinary project files/);
|
|
116
122
|
assert.match(bashTool?.description ?? "", /\/etc\/sudoers/);
|
|
117
123
|
assert.doesNotMatch(bashTool?.description ?? "", /Do not use bash to create, move, rename, or delete project files/);
|
|
124
|
+
assert.equal(bashTool?.inputSchema?.properties?.timeout, undefined);
|
|
125
|
+
assert.match(bashTool?.description ?? "", /waits up to 300 seconds/);
|
|
126
|
+
assert.match(bashTool?.description ?? "", /write_stdin/);
|
|
127
|
+
const writeStdinTool = tools.find((tool) => tool.name === "write_stdin");
|
|
128
|
+
assert.equal(writeStdinTool?.inputSchema?.properties?.yieldTimeMs?.maximum, 300000);
|
|
129
|
+
const openWorkspaceTool = tools.find((tool) => tool.name === "open_workspace");
|
|
130
|
+
assert.ok(openWorkspaceTool?.inputSchema?.properties?.workspaceId);
|
|
131
|
+
assert.ok(openWorkspaceTool?.inputSchema?.properties?.newWorkspace);
|
|
132
|
+
assert.ok(openWorkspaceTool?.outputSchema?.properties?.staleWorkspaces);
|
|
133
|
+
const templateUri = bashTool?._meta?.ui?.resourceUri;
|
|
134
|
+
assert.match(
|
|
135
|
+
templateUri ?? "",
|
|
136
|
+
/^ui:\/\/forgerelay\/workspace-app-[0-9a-f]{12}\.html$/,
|
|
137
|
+
JSON.stringify(bashTool ?? {}),
|
|
138
|
+
);
|
|
139
|
+
assert.deepEqual(bashTool?._meta?.ui?.visibility, ["model", "app"]);
|
|
140
|
+
assert.equal(bashTool?._meta?.["openai/outputTemplate"], templateUri);
|
|
118
141
|
pass("MCP tools/list", `${toolNames.length} tools: ${toolNames.join(", ")}`);
|
|
119
142
|
|
|
143
|
+
const resources = mcpRequest(oauth.accessToken, sessionId, {
|
|
144
|
+
jsonrpc: "2.0",
|
|
145
|
+
id: 21,
|
|
146
|
+
method: "resources/list",
|
|
147
|
+
params: {},
|
|
148
|
+
}).message.result.resources;
|
|
149
|
+
assert.ok(resources.some((resource) => resource.uri === templateUri));
|
|
150
|
+
assert.ok(resources.some((resource) => resource.uri === "ui://forgerelay/workspace-app.html"));
|
|
151
|
+
|
|
152
|
+
const resourceTemplates = mcpRequest(oauth.accessToken, sessionId, {
|
|
153
|
+
jsonrpc: "2.0",
|
|
154
|
+
id: 22,
|
|
155
|
+
method: "resources/templates/list",
|
|
156
|
+
params: {},
|
|
157
|
+
}).message.result.resourceTemplates;
|
|
158
|
+
assert.ok(resourceTemplates.some(
|
|
159
|
+
(resourceTemplate) => resourceTemplate.uriTemplate === "ui://forgerelay/workspace-app-{revision}.html",
|
|
160
|
+
));
|
|
161
|
+
|
|
162
|
+
const readTemplate = (id, uri) => mcpRequest(oauth.accessToken, sessionId, {
|
|
163
|
+
jsonrpc: "2.0",
|
|
164
|
+
id,
|
|
165
|
+
method: "resources/read",
|
|
166
|
+
params: { uri },
|
|
167
|
+
}).message.result.contents[0];
|
|
168
|
+
|
|
169
|
+
const template = readTemplate(23, templateUri);
|
|
170
|
+
assert.equal(template.uri, templateUri);
|
|
171
|
+
assert.equal(template.mimeType, "text/html;profile=mcp-app");
|
|
172
|
+
assert.match(template.text ?? "", /<script type="module" crossorigin src="[^"]+\/mcp-app-assets\//);
|
|
173
|
+
assert.ok(template._meta?.ui?.csp?.resourceDomains?.includes(debugBaseUrl));
|
|
174
|
+
const scriptUrl = template.text?.match(/<script type="module" crossorigin src="([^"]+)"/)?.[1];
|
|
175
|
+
assert.ok(scriptUrl);
|
|
176
|
+
const scriptAsset = curlRequest({ method: "GET", url: scriptUrl });
|
|
177
|
+
assert.equal(scriptAsset.status, 200, scriptAsset.body);
|
|
178
|
+
|
|
179
|
+
const legacyTemplate = readTemplate(24, "ui://forgerelay/workspace-app.html");
|
|
180
|
+
assert.equal(legacyTemplate.uri, "ui://forgerelay/workspace-app.html");
|
|
181
|
+
assert.equal(legacyTemplate.mimeType, "text/html;profile=mcp-app");
|
|
182
|
+
assert.equal(legacyTemplate.text, template.text);
|
|
183
|
+
|
|
184
|
+
const historicalUri = "ui://forgerelay/workspace-app-0.2.4.html";
|
|
185
|
+
const historicalTemplate = readTemplate(25, historicalUri);
|
|
186
|
+
assert.equal(historicalTemplate.uri, historicalUri);
|
|
187
|
+
assert.equal(historicalTemplate.mimeType, "text/html;profile=mcp-app");
|
|
188
|
+
assert.equal(historicalTemplate.text, template.text);
|
|
189
|
+
pass("MCP app template", `${templateUri} + legacy/history compatibility -> ${scriptUrl}`);
|
|
190
|
+
|
|
120
191
|
const opened = callTool(oauth.accessToken, sessionId, 3, "open_workspace", {
|
|
121
192
|
path: checkoutWorkspace,
|
|
122
193
|
});
|
|
@@ -145,7 +216,8 @@ try {
|
|
|
145
216
|
command: "printf debug-bash-ok",
|
|
146
217
|
});
|
|
147
218
|
assert.match(shell.structuredContent.result, /debug-bash-ok/);
|
|
148
|
-
|
|
219
|
+
assert.equal(shell.structuredContent.running, false);
|
|
220
|
+
pass("bash", "foreground command completed through ProcessSessionManager");
|
|
149
221
|
|
|
150
222
|
const failedEdit = callTool(oauth.accessToken, sessionId, 7, "edit", {
|
|
151
223
|
workspaceId,
|
|
@@ -354,7 +426,13 @@ function initializeRequest(id) {
|
|
|
354
426
|
method: "initialize",
|
|
355
427
|
params: {
|
|
356
428
|
protocolVersion: "2025-06-18",
|
|
357
|
-
capabilities: {
|
|
429
|
+
capabilities: {
|
|
430
|
+
extensions: {
|
|
431
|
+
"io.modelcontextprotocol/ui": {
|
|
432
|
+
mimeTypes: ["text/html;profile=mcp-app"],
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
},
|
|
358
436
|
clientInfo: { name: "forgerelay-debug-acceptance", version: "1.0.0" },
|
|
359
437
|
},
|
|
360
438
|
};
|
|
@@ -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:
|
|
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,
|