@akira-tl/forgerelay 0.2.6 → 0.3.0
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 +18 -0
- package/README.md +15 -0
- package/capabilities/artifacts-review/GUIDE.md +42 -0
- package/capabilities/host-integration/GUIDE.md +68 -0
- package/capabilities/lifecycle-hooks/GUIDE.md +39 -0
- package/capabilities/managed-worktrees/GUIDE.md +43 -0
- package/capabilities/shell-processes/GUIDE.md +51 -0
- package/capabilities/subagents/GUIDE.md +69 -0
- package/dist/advertised-files.js +23 -0
- package/dist/capabilities.js +99 -0
- package/dist/cli.js +1 -3
- package/dist/config.js +11 -3
- package/dist/logger.js +1 -12
- package/dist/mcp/server-instructions.js +15 -30
- package/dist/server.js +42 -13
- package/dist/skills.js +11 -30
- package/dist/workspaces.js +16 -0
- package/docs/chatgpt-coding-workflow.md +55 -13
- package/docs/configuration.md +45 -2
- package/docs/roadmap.md +29 -3
- package/docs/security.md +24 -7
- package/package.json +4 -3
- package/scripts/debug/accept.mjs +38 -3
- package/scripts/ensure-cli-executable.mjs +12 -0
package/docs/roadmap.md
CHANGED
|
@@ -102,7 +102,33 @@ Hooks v1 的目标是给用户和 Agent 一个很小、自动、可组合的生
|
|
|
102
102
|
- 后台命令句柄以 `processId` / process handle 为 canonical 名称,旧 `sessionId` 在 0.2.x 保留 deprecated alias 兼容窗口;
|
|
103
103
|
- 为 stateless MCP transport 做准备,同时保留旧协议兼容 adapter,避免把 transport 生命周期重新提升成 ForgeRelay 会话模型。
|
|
104
104
|
|
|
105
|
-
## 0.3 —
|
|
105
|
+
## 0.3 — MCP loading 与渐进式能力披露
|
|
106
|
+
|
|
107
|
+
0.3 的目标是缩小 MCP 首次加载时注入给 Agent 的上下文,同时保持工具可调用性、安全边界和 Host 编排权不变。ForgeRelay 不再把所有低频能力说明都塞进 server instructions 或工具 description,而是把模型接口拆成三层:
|
|
108
|
+
|
|
109
|
+
- `tools/list` 继续暴露真实可调用 primitive,并只携带调用该工具所必需的简洁语义;
|
|
110
|
+
- `open_workspace` 返回紧凑的 server/capability 摘要与版本指纹,帮助 Agent 发现能力,并识别 Host 持有旧 tool schema snapshot 的情况;
|
|
111
|
+
- ForgeRelay-owned capability guide 提供低频能力的完整说明,由 Agent 在任务相关时显式 `read`,而不是首次连接时自动注入。
|
|
112
|
+
|
|
113
|
+
首版优先复用现有 Skill-style 的 advertised path / `read` 授权机制,而不是新增第二套文档加载协议。Capability guide 与 Skill 的语义所有权保持区分:Skill 描述用户、项目或生态工作流;capability guide 描述 ForgeRelay 自身、与版本绑定的产品能力。
|
|
114
|
+
|
|
115
|
+
Core Capability Contract 必须始终内联保留至少这些信息:
|
|
116
|
+
|
|
117
|
+
- `workspaceId` 生命周期与 workspace 复用规则;
|
|
118
|
+
- 常用文件读写改、`rename` 同时承担 move/rename、删除的核心语义;
|
|
119
|
+
- shell 以本地用户权限执行且不是 OS sandbox;
|
|
120
|
+
- `processId` / `write_stdin` 的基本长进程语义;
|
|
121
|
+
- Hook 阻断结果必须对 Agent 可见;
|
|
122
|
+
- 关键 mutation/safety invariant;
|
|
123
|
+
- `close_workspace` 与 `close_worktree` 的区别。
|
|
124
|
+
|
|
125
|
+
适合按需读取的首批领域包括:生命周期 Hooks、managed worktree 高级流程、subagents、artifact/review 工作流、debug/MCP App、OAuth/deployment,以及 shell/PTTY/process 的低频边界情况。首个实现切片迁移 Hooks 与 managed worktree 高级说明;第二切片继续覆盖 subagents、artifact/review、Host/OAuth/MCP App integration 与 shell/PTTY/process,并把历史 bundled `subagent-delegation` Skill 的默认自动发现迁回 ForgeRelay-owned capability guide。必要安全语义始终保留在 core contract 或真实 tool schema/description 中。
|
|
126
|
+
|
|
127
|
+
Capability/version fingerprint 必须是轻量、语义化、稳定的摘要,不复制完整 `tools/list`。当 server 报告的能力与 Host 当前暴露的 tool snapshot 明显不一致时,Agent 应能判断为 Host metadata stale,并建议刷新 MCP 或开启新会话,而不是错误断言 ForgeRelay 缺少能力。
|
|
128
|
+
|
|
129
|
+
0.3 不隐藏 callable tool,不增加隐式 autonomous workflow,也不把 Host Refresh/session 行为归到 ForgeRelay。`rename` 继续作为文件和目录 move/rename 的统一 primitive。
|
|
130
|
+
|
|
131
|
+
## 0.4 — LSP code intelligence v1
|
|
106
132
|
|
|
107
133
|
LSP is moderate implementation complexity if ForgeRelay does not become a
|
|
108
134
|
language-server installer.
|
|
@@ -110,7 +136,7 @@ language-server installer.
|
|
|
110
136
|
The first version should launch only language servers already available on the
|
|
111
137
|
user's machine or explicitly configured by the user/project.
|
|
112
138
|
|
|
113
|
-
During 0.
|
|
139
|
+
During 0.4 development, MCP App UI hardening can land alongside the LSP work when
|
|
114
140
|
it does not distort the code-intelligence scope. In particular, evaluate a more
|
|
115
141
|
self-contained template/bootstrap bundle, reduce avoidable external chunk fetches,
|
|
116
142
|
and keep the current content-hash/compatibility-resource contract intact. This is
|
|
@@ -144,7 +170,7 @@ Candidate servers include `typescript-language-server`/tsserver, Pyright,
|
|
|
144
170
|
`rust-analyzer`, `gopls`, and `clangd`, but ForgeRelay should treat server
|
|
145
171
|
commands/configuration as external dependencies.
|
|
146
172
|
|
|
147
|
-
## 0.
|
|
173
|
+
## 0.5 — First-class subagent MCP
|
|
148
174
|
|
|
149
175
|
ForgeRelay already owns provider adapters and resumable local agent sessions.
|
|
150
176
|
The next step is to remove the current `bash -> forgerelay agents ...` indirection
|
package/docs/security.md
CHANGED
|
@@ -28,6 +28,17 @@ Filesystem-oriented tools canonicalize existing path segments before access so
|
|
|
28
28
|
symlinks inside either the workspace or OS temp directory cannot escape to
|
|
29
29
|
arbitrary filesystem locations.
|
|
30
30
|
|
|
31
|
+
`read` has one narrow additional path class for explicitly advertised documents.
|
|
32
|
+
`open_workspace` may advertise a Skill entry file or a ForgeRelay capability
|
|
33
|
+
Guide outside the normal workspace/temp roots. Only that advertised entry file is
|
|
34
|
+
readable initially; after it is read, files inside that advertised document's
|
|
35
|
+
own directory may be read as supporting material. This does not expand write,
|
|
36
|
+
edit, rename, delete, shell working-directory, or workspace-open roots.
|
|
37
|
+
|
|
38
|
+
Capability Guides are ForgeRelay-owned, versioned package documentation. Their
|
|
39
|
+
paths are surfaced by the running server rather than guessed by the Agent. This
|
|
40
|
+
keeps progressive disclosure from becoming a general arbitrary-file read escape.
|
|
41
|
+
|
|
31
42
|
## Owner-password OAuth
|
|
32
43
|
|
|
33
44
|
New installations store local configuration in:
|
|
@@ -100,13 +111,19 @@ for dynamically crossing such a boundary.
|
|
|
100
111
|
The Agent-facing shell contract therefore does not ban every command that can
|
|
101
112
|
change files. Commands may modify ordinary project files when that is a natural
|
|
102
113
|
part of the user's requested development task, including package managers,
|
|
103
|
-
generators, formatters, and similar tooling.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
114
|
+
generators, formatters, and similar tooling. Shell commands may also perform
|
|
115
|
+
external device or hardware mutations when the user's current request explicitly
|
|
116
|
+
asks for the actual device-changing operation. ForgeRelay does not assume a
|
|
117
|
+
particular flashing protocol, transport, device path, or firmware workflow. A
|
|
118
|
+
check, audit, probe, backup, verification, dry-run, or build-only request must not
|
|
119
|
+
be treated as authorization for a later persistent hardware write.
|
|
120
|
+
|
|
121
|
+
The contract does prohibit shell mutation of security- or privilege-sensitive
|
|
122
|
+
operating-system files and credential material such as `/etc/sudoers`,
|
|
123
|
+
`/etc/passwd`, `/etc/shadow`, PAM or authentication policy, SSH private keys, and
|
|
124
|
+
equivalent privileged targets. Configuration files may be changed through shell
|
|
125
|
+
only when the user's request explicitly calls for that configuration change
|
|
126
|
+
rather than merely making it a convenient implementation detail.
|
|
110
127
|
|
|
111
128
|
This is an Agent execution policy, not an OS-level sandbox or command parser.
|
|
112
129
|
Operators that need a stronger project-specific runtime gate can use blocking
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akira-tl/forgerelay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"examples",
|
|
25
25
|
"scripts",
|
|
26
26
|
"skills",
|
|
27
|
+
"capabilities",
|
|
27
28
|
"README.md",
|
|
28
29
|
"CHANGELOG.md",
|
|
29
30
|
"NOTICE.md"
|
|
@@ -34,14 +35,14 @@
|
|
|
34
35
|
},
|
|
35
36
|
"scripts": {
|
|
36
37
|
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
37
|
-
"build": "npm run clean && npm run build:app && tsc -p tsconfig.build.json",
|
|
38
|
+
"build": "npm run clean && npm run build:app && tsc -p tsconfig.build.json && node scripts/ensure-cli-executable.mjs",
|
|
38
39
|
"build:app": "vite build",
|
|
39
40
|
"dev": "node scripts/debug/serve.mjs",
|
|
40
41
|
"debug:serve": "node scripts/debug/serve.mjs",
|
|
41
42
|
"debug:accept": "node scripts/debug/accept.mjs",
|
|
42
43
|
"postinstall": "node scripts/fix-node-pty-permissions.mjs",
|
|
43
44
|
"start": "node dist/cli.js serve",
|
|
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
|
+
"test": "tsx src/config.test.ts && tsx src/logger.test.ts && tsx src/proxy-trust.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
46
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
46
47
|
"release:check": "node scripts/release-version.mjs check",
|
|
47
48
|
"release:tag-check": "node scripts/release-version.mjs tag",
|
package/scripts/debug/accept.mjs
CHANGED
|
@@ -93,6 +93,9 @@ try {
|
|
|
93
93
|
assert.match(serverInstructions, /Shell commands may modify ordinary project files/);
|
|
94
94
|
assert.match(serverInstructions, /\/etc\/sudoers/);
|
|
95
95
|
assert.match(serverInstructions, /configuration files through shell only when the user's request explicitly calls for that configuration change/);
|
|
96
|
+
assert.match(serverInstructions, /managed-worktrees capability guide/);
|
|
97
|
+
assert.ok(serverInstructions.length < 3_000, `server instructions should stay compact, got ${serverInstructions.length} characters`);
|
|
98
|
+
assert.doesNotMatch(serverInstructions, /fast-forwards the original target branch/);
|
|
96
99
|
assert.doesNotMatch(serverInstructions, /Do not create or modify files with bash/);
|
|
97
100
|
pass(
|
|
98
101
|
"MCP initialize",
|
|
@@ -118,8 +121,10 @@ try {
|
|
|
118
121
|
assert.ok(toolNames.includes(expected), `missing debug tool ${expected}`);
|
|
119
122
|
}
|
|
120
123
|
const bashTool = tools.find((tool) => tool.name === "bash");
|
|
121
|
-
assert.match(bashTool?.description ?? "", /
|
|
122
|
-
assert.
|
|
124
|
+
assert.match(bashTool?.description ?? "", /local user's authority/);
|
|
125
|
+
assert.doesNotMatch(bashTool?.description ?? "", /may modify ordinary project files/);
|
|
126
|
+
assert.doesNotMatch(bashTool?.description ?? "", /\/etc\/sudoers/);
|
|
127
|
+
assert.doesNotMatch(bashTool?.description ?? "", /external device or hardware mutations/);
|
|
123
128
|
assert.doesNotMatch(bashTool?.description ?? "", /Do not use bash to create, move, rename, or delete project files/);
|
|
124
129
|
assert.equal(bashTool?.inputSchema?.properties?.timeout, undefined);
|
|
125
130
|
assert.match(bashTool?.description ?? "", /waits up to 300 seconds/);
|
|
@@ -138,6 +143,8 @@ try {
|
|
|
138
143
|
assert.ok(openWorkspaceTool?.inputSchema?.properties?.workspaceId);
|
|
139
144
|
assert.ok(openWorkspaceTool?.inputSchema?.properties?.newWorkspace);
|
|
140
145
|
assert.ok(openWorkspaceTool?.outputSchema?.properties?.staleWorkspaces);
|
|
146
|
+
assert.ok(openWorkspaceTool?.outputSchema?.properties?.capabilityFingerprint);
|
|
147
|
+
assert.ok(openWorkspaceTool?.outputSchema?.properties?.capabilityGuides);
|
|
141
148
|
const templateUri = bashTool?._meta?.ui?.resourceUri;
|
|
142
149
|
assert.match(
|
|
143
150
|
templateUri ?? "",
|
|
@@ -203,7 +210,35 @@ try {
|
|
|
203
210
|
assert.match(workspaceId, /^ws_/);
|
|
204
211
|
assert.equal(opened.structuredContent.root, checkoutWorkspace);
|
|
205
212
|
assert.equal(opened.structuredContent.mode, "checkout");
|
|
206
|
-
|
|
213
|
+
assert.deepEqual(opened.structuredContent.capabilityFingerprint, {
|
|
214
|
+
version: packageJson.version,
|
|
215
|
+
toolMode: "full",
|
|
216
|
+
capabilities: [
|
|
217
|
+
"workspace.close",
|
|
218
|
+
"worktree.managed",
|
|
219
|
+
"filesystem.rename-move",
|
|
220
|
+
"filesystem.delete",
|
|
221
|
+
"process.write-stdin",
|
|
222
|
+
"hooks.lifecycle",
|
|
223
|
+
"capability-guides.read",
|
|
224
|
+
"inspection.search-tools",
|
|
225
|
+
"ui.mcp-app",
|
|
226
|
+
],
|
|
227
|
+
});
|
|
228
|
+
const capabilityGuides = opened.structuredContent.capabilityGuides;
|
|
229
|
+
assert.deepEqual(capabilityGuides.map((guide) => guide.name), [
|
|
230
|
+
"lifecycle-hooks",
|
|
231
|
+
"managed-worktrees",
|
|
232
|
+
"host-integration",
|
|
233
|
+
"shell-processes",
|
|
234
|
+
]);
|
|
235
|
+
const hooksGuide = callTool(oauth.accessToken, sessionId, 78, "read", {
|
|
236
|
+
workspaceId,
|
|
237
|
+
path: capabilityGuides[0].path,
|
|
238
|
+
});
|
|
239
|
+
assert.match(hooksGuide.structuredContent.result, /BeforeTool/);
|
|
240
|
+
assert.match(hooksGuide.structuredContent.result, /BeforeWorktreeClose/);
|
|
241
|
+
pass("open_workspace", `${workspaceId} -> fingerprint + ${capabilityGuides.length} capability guides`);
|
|
207
242
|
|
|
208
243
|
const written = callTool(oauth.accessToken, sessionId, 4, "write", {
|
|
209
244
|
workspaceId,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { chmodSync, statSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
4
|
+
if (process.platform !== "win32") {
|
|
5
|
+
const cliPath = fileURLToPath(new URL("../dist/cli.js", import.meta.url));
|
|
6
|
+
const currentMode = statSync(cliPath).mode;
|
|
7
|
+
chmodSync(cliPath, currentMode | 0o111);
|
|
8
|
+
|
|
9
|
+
if ((statSync(cliPath).mode & 0o111) === 0) {
|
|
10
|
+
throw new Error(`ForgeRelay CLI is not executable after build: ${cliPath}`);
|
|
11
|
+
}
|
|
12
|
+
}
|