@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/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 — LSP code intelligence v1
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.3 development, MCP App UI hardening can land alongside the LSP work when
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.4 — First-class subagent MCP
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. The contract does prohibit shell
104
- mutation of security- or privilege-sensitive operating-system files and
105
- credential material such as `/etc/sudoers`, `/etc/passwd`, `/etc/shadow`, PAM or
106
- authentication policy, SSH private keys, and equivalent privileged targets.
107
- Configuration files may be changed through shell only when the user's request
108
- explicitly calls for that configuration change rather than merely making it a
109
- convenient implementation detail.
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.2.6",
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",
@@ -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 ?? "", /may modify ordinary project files/);
122
- assert.match(bashTool?.description ?? "", /\/etc\/sudoers/);
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
- pass("open_workspace", `${workspaceId} -> ${opened.structuredContent.root}`);
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
+ }