@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 CHANGED
@@ -4,6 +4,24 @@ All notable ForgeRelay changes are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.3.0] - 2026-08-10
8
+
9
+ ### Added
10
+
11
+ - `open_workspace` now reports a lightweight ForgeRelay version/capability fingerprint on every open, allowing Agents to distinguish a stale Host tool-schema snapshot from a missing server capability.
12
+ - ForgeRelay-owned capability guides can be loaded explicitly with the normal `read` tool. Built-in guides cover lifecycle Hooks, managed worktrees, subagents, artifact/change review, Host/OAuth/MCP App integration, and long-running shell/PTY/process behavior; optional guides are advertised only when their feature is enabled.
13
+
14
+ ### Changed
15
+
16
+ - MCP bootstrap context now keeps low-frequency operational detail out of server instructions while `tools/list` remains the source of truth for the real callable tool surface. Artifact/review feature flags no longer inflate the core instruction payload.
17
+ - New setups no longer auto-seed or auto-inject the historical bundled `subagent-delegation` Skill; ForgeRelay's own subagent workflow is documented by the on-demand `subagents` capability guide. Existing user-authored or previously seeded Skills remain supported.
18
+ - Shell capability instructions now explicitly permit persistent external device or hardware mutations only when the user's current request asks for the actual device-changing operation; checks, audits, probes, backups, verification, dry-runs, and build-only requests do not implicitly authorize a later hardware write.
19
+
20
+ ### Fixed
21
+
22
+ - Loopback deployments behind a public tunnel/reverse proxy now trust exactly one upstream proxy hop by default, preventing MCP SDK OAuth rate limiting from emitting `ERR_ERL_UNEXPECTED_X_FORWARDED_FOR`; explicit proxy trust also no longer maps to Express's unsafe blanket `trust proxy = true` mode.
23
+ - Local checkout builds now restore the executable bit on `dist/cli.js`, so `npm run build && npm install -g .` produces a runnable `forgerelay` command on POSIX systems.
24
+
7
25
  ## [0.2.6] - 2026-08-10
8
26
 
9
27
  ### Changed
package/README.md CHANGED
@@ -97,6 +97,21 @@ Once a workspace is open, the host can:
97
97
  Normal work happens in your existing checkout. ForgeRelay does not silently move
98
98
  every task into a worktree.
99
99
 
100
+ ### Progressive MCP context
101
+
102
+ ForgeRelay keeps the callable MCP surface and its low-frequency operating manuals
103
+ separate. `tools/list` remains the source of truth for tools the current server
104
+ actually exposes. `open_workspace` returns a compact version/capability fingerprint
105
+ and, on bootstrap, short descriptors for relevant ForgeRelay capability guides.
106
+ The Agent can load a task-specific guide with `read` instead of receiving every
107
+ Hook, worktree, subagent, artifact, OAuth, MCP App, PTY, and process edge case on
108
+ every connection.
109
+
110
+ The fingerprint also helps diagnose stale MCP Host metadata: if the running server
111
+ reports a semantic capability but the Host still shows an older tool snapshot,
112
+ refresh or reconnect the integration rather than assuming the capability is
113
+ missing from ForgeRelay.
114
+
100
115
  ## Worktrees without the usual cleanup mess
101
116
 
102
117
  A new managed worktree gets its own `forgerelay/*` branch instead of a detached
@@ -0,0 +1,42 @@
1
+ # ForgeRelay Artifacts and Change Review
2
+
3
+ 当任务需要把 MCP Host 提供的原生文件保存进 workspace,或需要使用 ForgeRelay 的聚合 change-review UI 时读取本指南。
4
+
5
+ ## Native artifact transfer
6
+
7
+ Native artifact download 默认关闭。只有 `tools/list` 实际暴露 `download_artifact` 时才调用它;当前安全发布实现主要面向 Linux。
8
+
9
+ 典型流程:
10
+
11
+ 1. 先用 `open_workspace` 打开目标项目;
12
+ 2. 调用 `download_artifact`,传入 Host 原样提供的 native `file` 值、现有 `workspaceId` 和 workspace-relative `path`;
13
+ 3. 成功后使用返回的 normalized relative path 配合普通 `read` / `rename` / `delete` / shell 等工具继续处理。
14
+
15
+ `download_artifact` 会创建缺失的父目录,但拒绝覆盖已有目标。不要把 native file 值改造成:
16
+
17
+ - 任意 URL 字符串;
18
+ - 猜测的本地主机路径;
19
+ - base64 内容;
20
+ - 含凭据的扩展对象。
21
+
22
+ 不要把 signed URL、native connector object、base64 或凭据复制进 shell command 或日志。需要显式替换、移动、重命名或删除时,等文件安全落到 workspace 后再用普通 workspace primitives。
23
+
24
+ 默认单文件上限为 100 MiB,可通过 `FORGERELAY_ARTIFACT_MAX_FILE_BYTES` 调整。ForgeRelay 先流式写入私有 partial,再校验并无覆盖发布;它不是任意 URL downloader,也没有 TTL/pinning/background cleanup 语义。
25
+
26
+ ## Change review modes
27
+
28
+ `FORGERELAY_WIDGETS`:
29
+
30
+ - `full`:默认,为常用 workspace/file/edit/shell tools 附加 MCP App UI;
31
+ - `changes`:聚焦 `open_workspace` + `show_changes` 聚合 review;
32
+ - `off`:不附加 widget UI。
33
+
34
+ Plain MCP Host 可以忽略这些 UI metadata;不要把 UI 是否显示当成文件操作是否成功的判据。
35
+
36
+ 当 `tools/list` 暴露 `show_changes` 时,它自己的 tool description 是调用契约:本轮成功修改文件后,在最后一个相关 file mutation 之后、final response 之前调用一次,让用户看到聚合 diff;不要每改一个文件就调用一次。
37
+
38
+ `show_changes` 使用 Git-backed review checkpoint。它按 workspace 跟踪 open/baseline 状态,展示自上次 review checkpoint 以来的 coherent diff,并在成功 review 后推进 baseline。当前版本要求可用的 Git workspace;checkpoint 缺失或 root 不匹配时会明确失败或使用受控 fallback,而不是凭空重建历史。
39
+
40
+ ## Capability ownership
41
+
42
+ Artifact transfer 和 change review 都属于 ForgeRelay capability,而不是 Agent 自己的文件搬运协议。真实可调用工具仍以 `tools/list` 为准;本指南只提供低频流程和边界,不代表隐藏工具。
@@ -0,0 +1,68 @@
1
+ # ForgeRelay Host Integration
2
+
3
+ 当任务涉及 MCP Host 连接、OAuth、public URL / tunnel、Host tool metadata 不同步,或 MCP App UI/template 排障时读取本指南。
4
+
5
+ ## Public endpoint
6
+
7
+ ForgeRelay 本地监听一个 HTTP origin,MCP endpoint 是 `/mcp`。当 Host 无法直接访问 localhost 时,需要用户自己提供 public HTTPS tunnel/reverse proxy;ForgeRelay 不创建 tunnel。
8
+
9
+ 持久化的 `publicBaseUrl` 必须是 origin,不要包含 `/mcp`:
10
+
11
+ ```text
12
+ publicBaseUrl: https://forge.example.com
13
+ Host MCP URL: https://forge.example.com/mcp
14
+ ```
15
+
16
+ 临时覆盖可使用 `FORGERELAY_PUBLIC_BASE_URL`。Host-header/403 问题先运行 `forgerelay doctor` 检查 resolved public URL 与 allowed hosts;`FORGERELAY_ALLOWED_HOSTS="*"` 只适合明确的本地调试。
17
+
18
+ 当 ForgeRelay 绑定 loopback、但 `publicBaseUrl` 指向公网 tunnel/reverse proxy 时,ForgeRelay 会自动信任恰好 1 个上游代理 hop,让 Express 与 OAuth rate limiter 使用一致的客户端 IP。不要把 Express `trust proxy` 设为无条件 `true`;直接监听 `0.0.0.0` 等非 loopback 地址时也不会自动开启代理信任。可用 `FORGERELAY_TRUST_PROXY=0|1` 显式覆盖。
19
+
20
+ ## OAuth owner flow
21
+
22
+ ForgeRelay 使用 single-user Owner-password OAuth approval flow。新安装的 owner secret 通常保存在 `~/.forgerelay/auth.json`;迁移安装可能继续使用旧 `.devspace` 配置目录。不要把 owner token、refresh token 或 `auth.json` 内容放进 Agent 输出、项目文件或日志。
23
+
24
+ Host 可通过这些 metadata endpoint 发现授权配置:
25
+
26
+ ```text
27
+ /.well-known/oauth-protected-resource/mcp
28
+ /.well-known/oauth-authorization-server
29
+ ```
30
+
31
+ 默认 redirect hosts 包含 `chatgpt.com`、`localhost`、`127.0.0.1`。其他 MCP client 需要显式加入 `FORGERELAY_OAUTH_ALLOWED_REDIRECT_HOSTS`。OAuth scope 默认仍使用 legacy-compatible internal identifier `devspace`,这是协议兼容值,不是产品名回退。
32
+
33
+ Owner password 不被接受时先用 `forgerelay doctor` 确认实际 auth/config 路径;只有用户明确要重建配置时才使用 `forgerelay init --force`。
34
+
35
+ ## Stale Host metadata
36
+
37
+ `open_workspace` 每次都返回 ForgeRelay version/capability fingerprint;真实 callable surface 仍由当前 server 的 `tools/list` 决定。
38
+
39
+ 如果 server fingerprint 明确报告某项已实现能力,而 Host 当前会话暴露的 tool snapshot 明显更旧,不要断言 ForgeRelay 缺少该能力。应判断为 Host MCP metadata stale,并建议重新连接/刷新 integration,或开启能重新加载 `tools/list` 的 Host context。ForgeRelay server 不能强制 Host 清掉缓存的 schema。
40
+
41
+ ## MCP App / template debugging
42
+
43
+ 当 ChatGPT 或其他支持 MCP App 的 Host 出现 `Failed to fetch template`、空卡片或资源加载失败时,先验证 server 侧链路:
44
+
45
+ ```bash
46
+ npm run build
47
+ npm run debug:accept
48
+ ```
49
+
50
+ ForgeRelay 正常会广告 content-hashed:
51
+
52
+ ```text
53
+ ui://forgerelay/workspace-app-<hash>.html
54
+ ```
55
+
56
+ `resources/read` 应返回 `text/html;profile=mcp-app`,并且 HTML 引用的 `/mcp-app-assets/` 资源必须可达。ForgeRelay 还保留 legacy `ui://forgerelay/workspace-app.html` 和历史 `workspace-app-*.html` 兼容指针,以容忍 Host 暂时持有旧 metadata snapshot。
57
+
58
+ 需要 live trace 时,可用:
59
+
60
+ ```bash
61
+ FORGERELAY_DEBUG_WIDGETS=full \
62
+ FORGERELAY_LOG_LEVEL=debug \
63
+ FORGERELAY_LOG_REQUESTS=1 \
64
+ FORGERELAY_LOG_ASSETS=1 \
65
+ forgerelay serve
66
+ ```
67
+
68
+ 排障时区分三类问题:Host 根本没有发 `resources/read`、template callback 失败、或 `/mcp-app-assets/` fetch 失败。不要把这三者混成同一个“UI坏了”。
@@ -0,0 +1,39 @@
1
+ # ForgeRelay 生命周期 Hooks
2
+
3
+ 当任务涉及新增、修改、排查或解释 ForgeRelay Hook 时读取本指南。Hook 是 ForgeRelay 自己的生命周期规则,不是权限审批系统,也不是 Git hook。
4
+
5
+ ## 配置位置
6
+
7
+ - 全局 Hook:ForgeRelay 配置目录下的 `hooks/<hook-name>.json`。
8
+ - 项目 Hook:工作区根目录下的 `.forgerelay/hooks/<hook-name>.json`。
9
+ - 一个文件定义一个 Hook;文件名去掉 `.json` 后就是 Hook 名。
10
+ - 同一作用域内按文件名字典序执行;需要显式顺序时可使用数字前缀。
11
+ - 全局与项目 Hook 会组合执行,项目规则不会覆盖机器级全局规则。
12
+
13
+ 旧的 inline `hooks`、全局 `hooks.json` 和项目 `.forgerelay/hooks.json` 聚合格式仍作为兼容入口读取。
14
+
15
+ ## 规则模型
16
+
17
+ 每个独立 Hook 文件包含:
18
+
19
+ - `event`:生命周期事件;
20
+ - 可选 `matcher`:只在匹配时执行;
21
+ - `command`:由 ForgeRelay 以本地用户权限执行的命令;
22
+ - 可选 timeout;
23
+ - 可选 `report`,默认 `true`。
24
+
25
+ 当前事件包括:`WorkspaceOpen`、`BeforeTool`、`AfterTool`、`AfterToolFailure`、`AfterFileChange`、`BeforeWorktreeClose`、`AfterWorktreeClose`、`SubagentStart`、`SubagentStop`。
26
+
27
+ ## 阻断与报告
28
+
29
+ `BeforeTool` 和 `BeforeWorktreeClose` 是阻断事件。命中的 Hook 失败或超时后,原操作不会继续。其他 after-event 只观察已经发生的结果,失败不会伪装成能够回滚先前副作用。
30
+
31
+ Hook report 会随工具结果返回给 Host/Agent。`report: false` 只隐藏成功的高频报告;阻断失败始终可见。Agent 看到有意义的 Hook report 时必须告诉用户哪些 Hook 运行了、是否通过,以及操作是否被阻断;不能在 blocking Hook 阻止操作后声称原操作成功。
32
+
33
+ ## 安全边界
34
+
35
+ 项目 Hook 属于项目执行约定,不需要额外审批,但不能扩大 allowed roots、覆盖认证边界或替换机器级全局规则。Hook command 与 shell 一样以运行 ForgeRelay 的本地用户权限执行;工作区文件边界不等于 OS sandbox。
36
+
37
+ ## 检查入口
38
+
39
+ 使用 `forgerelay hooks list` 查看已发现规则,使用 `forgerelay hooks check` 做只读校验。排查 Hook 时优先确认配置来源、event/matcher 是否命中、handler 的实际退出状态,以及 tool result 中的 Hook report。
@@ -0,0 +1,43 @@
1
+ # ForgeRelay managed worktree
2
+
3
+ 当任务需要隔离/并行 Git 工作,或需要排查 managed worktree 的创建、复用、关闭与恢复时读取本指南。普通开发默认使用 checkout mode;只有用户明确要求隔离或并行工作时才使用 `mode="worktree"`。
4
+
5
+ ## 基本模型
6
+
7
+ - `workspaceId` 是逻辑工作身份;managed worktree 是物理 Git worktree。二者不要混用。
8
+ - managed worktree 使用 ForgeRelay 管理的 `forgerelay/*` 分支,不使用 detached HEAD。
9
+ - 创建时会记录 source checkout、base ref/base SHA、managed branch 和 target branch。
10
+ - 同一个物理 worktree 可以存在多个逻辑 workspace handle;关闭逻辑 handle 与删除物理 worktree 是不同操作。
11
+
12
+ ## 打开与复用
13
+
14
+ 调用 `open_workspace` 时:
15
+
16
+ - 默认 `mode="checkout"`,直接使用用户已有 checkout;
17
+ - 只有明确需要隔离/并行 Git 工作时才选 `mode="worktree"`;
18
+ - `baseRef` 只用于 managed worktree,默认取 source checkout 当前分支;
19
+ - 已有 `workspaceId` 用于恢复同一个逻辑 workspace;
20
+ - `newWorkspace` 只创建新的逻辑 handle;
21
+ - `newWorktree` 才表示同一项目再创建一个独立物理 worktree。
22
+
23
+ 不要为了“更安全”自动选择 worktree,也不要在用户没有要求时创建额外 Git 分支。
24
+
25
+ ## `close_workspace` 与 `close_worktree`
26
+
27
+ `close_workspace` 只释放一个逻辑 `workspaceId`,不会删除 checkout 文件,也不会完成 managed branch 集成。若某个 managed worktree 仍有其他逻辑 handle,释放其中一个 handle 不会移除物理 worktree。
28
+
29
+ `close_worktree` 用于完成一个 managed worktree:
30
+
31
+ 1. 要求该 worktree 的工作已经完成并验证;
32
+ 2. 若仍有未提交修改,ForgeRelay 使用调用时提供的 commit message 提交;
33
+ 3. 只有 source checkout 干净、目标历史没有分叉且能够安全 fast-forward 时,才把 managed branch 集成到原 target branch;
34
+ 4. 成功后移除 worktree 目录和 ForgeRelay 管理分支;
35
+ 5. 若安全 fast-forward 不成立,不把 source checkout 留在 merge-conflict 状态,而是拒绝关闭并保留 worktree 供用户/Agent 处理。
36
+
37
+ 运行中的 process 或尚未消费的 process completion 也会阻止相关逻辑 workspace/worktree 被关闭。
38
+
39
+ ## 外部变化与恢复
40
+
41
+ source checkout 可能被用户或其他 Agent 修改。关闭前不要假设 target branch 仍停留在创建 worktree 时的状态;让 ForgeRelay 的 close 检查决定是否可以安全集成。遇到 diverged target、dirty source、忙碌 workspace 或 cleanup warning 时,保留现有 worktree 状态并向用户说明真实失败点,不要自行强制 reset、merge 或删除 worktree。
42
+
43
+ 如果需要继续工作,复用原 `workspaceId` 或已知 worktree path;不要因为一次关闭失败就创建新的隔离副本。
@@ -0,0 +1,51 @@
1
+ # ForgeRelay Shell and Processes
2
+
3
+ 当命令长时间运行、需要交互式 TTY、需要 `write_stdin`,或遇到 shell/process 平台边界问题时读取本指南。
4
+
5
+ ## Core process model
6
+
7
+ `bash`(Codex tool mode 下为 `exec_command`)在 open workspace 内启动命令。命令拥有本地用户权限;workspace path containment 不等于 OS sandbox。
8
+
9
+ 普通 `bash` 最多在前台等待 300 秒。如果进程仍存活,ForgeRelay 不会因为 wait window 到期而杀掉它,而是返回:
10
+
11
+ ```text
12
+ running: true
13
+ processId: <number>
14
+ ```
15
+
16
+ `processId` 是 canonical process handle。旧 `sessionId` 仅为 0.2.x compatibility alias,不应作为新代码或新 Agent workflow 的首选名称。
17
+
18
+ ## write_stdin
19
+
20
+ 使用同一个 `workspaceId` 和 `processId`:
21
+
22
+ - 省略 `chars` 或传空字符串:poll;
23
+ - 传普通字符:向正在运行的进程写入输入;
24
+ - 传 `\u0003`:显式发送 Ctrl-C;
25
+ - `yieldTimeMs`:继续等待,单次最多 300000 ms;
26
+ - `maxOutputTokens`:限制本次返回的近似输出 token;
27
+ - `columns` / `rows`:调整已经分配 PTY 的终端尺寸。
28
+
29
+ 等待超时不会隐式 kill process。若没有必要立即等待,可以继续其他工作;进程完成后,ForgeRelay 会把 completion notice 一次性附加到同一 logical workspace 的后续 tool result。
30
+
31
+ 不要因为暂时没有输出就重复启动相同长进程;先用返回的 `processId` poll。
32
+
33
+ ## PTY / interactive commands
34
+
35
+ 默认命令不需要 PTY。只有交互程序确实依赖 terminal semantics 时才设置 `tty: true`。Codex-shaped `exec_command` 可以同时指定:
36
+
37
+ ```text
38
+ tty: true
39
+ columns: 80
40
+ rows: 24
41
+ ```
42
+
43
+ PTY 依赖 optional `node-pty`。缺少该依赖时 ForgeRelay 会明确报错;不要把它误诊成命令本身失败。对非 PTY process 使用 `columns` / `rows` resize 也会失败。
44
+
45
+ 对需要 prompt/REPL 的程序,用 `tty: true` + `write_stdin`;对 tests/builds/formatters 等非交互命令保持默认非 PTY,以获得更稳定的 CI-style 输出。
46
+
47
+ ## Platform notes
48
+
49
+ ForgeRelay shell runtime 需要 Bash。Windows 上原生 PowerShell 和 `cmd.exe` 当前不是该 runtime 的执行 shell;使用 Git Bash、WSL、MSYS2 或 Cygwin Bash,并用 `forgerelay doctor` 检查环境。
50
+
51
+ Shell 可以作为用户开发任务的一部分修改普通项目文件,但始终受 ForgeRelay core mutation/safety contract 约束。涉及 privileged OS files、credentials、configuration 或外部硬件持久写入时,不要用本指南替代 core authorization/safety 规则。
@@ -0,0 +1,69 @@
1
+ # ForgeRelay Subagents
2
+
3
+ 当任务涉及委派给另一个本地 coding agent、获取第二意见、并行调查,或用户明确点名 subagent 时读取本指南。
4
+
5
+ ## 当前接口边界
6
+
7
+ 0.3 仍然使用 ForgeRelay CLI 协调 provider-backed local subagent;当前没有 first-class MCP subagent tool。不要把 Host 自己的 subagent 功能与 ForgeRelay subagent 混为一谈。
8
+
9
+ 启用方式:
10
+
11
+ ```bash
12
+ FORGERELAY_SUBAGENTS=1 forgerelay serve
13
+ ```
14
+
15
+ 启用后,`open_workspace` 会返回紧凑的 `agentProviders` 和 `agents` 元数据。配置 profile 通常来自全局 ForgeRelay 配置目录以及项目内:
16
+
17
+ ```text
18
+ .forgerelay/agents/*.md
19
+ .devspace/agents/*.md
20
+ ```
21
+
22
+ 旧 `.devspace` 路径仅用于迁移兼容。
23
+
24
+ ## 何时使用
25
+
26
+ 不要因为存在 profile 就自动委派普通开发工作。只有在用户要求委派、第二意见、并行工作或指定 subagent/provider 时才使用,并明确告诉用户正在使用另一个 subagent。
27
+
28
+ 选择 profile 时以 `open_workspace` 返回的 compact profile catalog 为准。`forgerelay agents ls` 显示的是当前 workspace 的已有 subagent sessions,不是 profile 定义列表。
29
+
30
+ ## 常用命令
31
+
32
+ 正常委派只需要:
33
+
34
+ ```bash
35
+ forgerelay agents ls
36
+ forgerelay agents run <profile-or-provider-or-id> "<prompt>"
37
+ forgerelay agents show <id>
38
+ ```
39
+
40
+ 语义:
41
+
42
+ - `run <profile>`:启动配置好的 profile;
43
+ - `run <provider>`:没有合适 profile 时直接启动 ForgeRelay 内建 provider;
44
+ - `run <id>`:向现有 agent session 发送 follow-up;
45
+ - `show <id>`:查看状态和最新响应;仍在运行时可稍后再次调用;
46
+ - shell workspace 环境会把 CLI 操作自动限定到当前 ForgeRelay workspace。
47
+
48
+ 如确实需要覆盖 profile/provider 默认值,可使用:
49
+
50
+ ```bash
51
+ forgerelay agents run <profile-or-provider> --model <model> "<prompt>"
52
+ forgerelay agents run <profile-or-provider> --thinking <level> "<prompt>"
53
+ ```
54
+
55
+ `thinking` 是 provider-specific passthrough;ForgeRelay 不在 provider 之间翻译 reasoning level。
56
+
57
+ 除非正在明确调试 ForgeRelay provider integration,不要绕过 ForgeRelay 直接运行 `codex`、`claude`、`opencode`、`pi`、`cursor-agent`、`copilot` 等 provider CLI。
58
+
59
+ ## Prompt 与验证
60
+
61
+ Subagent 只收到你发送的 prompt 和它自己的 profile instructions,因此 prompt 必须自包含:目标、相关模块/文件、约束、验收标准,以及它能否修改文件。
62
+
63
+ Subagent 的结果不是自动验证过的最终答案。收到响应后:
64
+
65
+ - 写入型任务:检查实际改动并运行相关测试;
66
+ - 只读调查:核对关键结论是否有仓库证据;
67
+ - 向用户说明使用了哪个 profile/provider、它给出的结论、你做了哪些验证,以及剩余风险。
68
+
69
+ `SubagentStart` / `SubagentStop` lifecycle Hooks 会在 worker 生命周期中触发;异步报告会随 ForgeRelay session 持久化并可由 `forgerelay agents show` 查看。
@@ -0,0 +1,23 @@
1
+ import { resolve } from "node:path";
2
+ import { expandHomePath, isPathInsideRoot } from "./roots.js";
3
+ export function resolveAdvertisedFileReadPath(sources, activatedDirs, inputPath) {
4
+ const absolutePath = resolve(expandHomePath(inputPath));
5
+ for (const source of sources) {
6
+ const entryFilePath = resolve(source.filePath);
7
+ if (absolutePath === entryFilePath) {
8
+ return { absolutePath, source, isEntryFile: true };
9
+ }
10
+ }
11
+ for (const source of sources) {
12
+ const baseDir = resolve(source.baseDir);
13
+ if (!activatedDirs.has(baseDir))
14
+ continue;
15
+ if (!isPathInsideRoot(absolutePath, baseDir))
16
+ continue;
17
+ return { absolutePath, source, isEntryFile: false };
18
+ }
19
+ return undefined;
20
+ }
21
+ export function markAdvertisedFileSourceActivated(activatedDirs, source) {
22
+ activatedDirs.add(resolve(source.baseDir));
23
+ }
@@ -0,0 +1,99 @@
1
+ import { join } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { markAdvertisedFileSourceActivated, resolveAdvertisedFileReadPath, } from "./advertised-files.js";
4
+ const CAPABILITY_GUIDE_DEFINITIONS = [
5
+ {
6
+ name: "lifecycle-hooks",
7
+ description: "Hook events, blocking, reports, and configuration.",
8
+ whenToRead: "Read for ForgeRelay Hook setup or debugging.",
9
+ },
10
+ {
11
+ name: "managed-worktrees",
12
+ description: "Managed-worktree lifecycle, close safety, and recovery.",
13
+ whenToRead: "Read for advanced mode=\"worktree\" flows.",
14
+ },
15
+ {
16
+ name: "subagents",
17
+ description: "Local subagent delegation and session follow-up.",
18
+ whenToRead: "Read when the user asks to delegate or use another coding agent.",
19
+ enabled: (config) => config.subagents,
20
+ },
21
+ {
22
+ name: "artifacts-review",
23
+ description: "Native artifact transfer and aggregate change review.",
24
+ whenToRead: "Read for host-provided files or show_changes.",
25
+ enabled: (config) => config.artifactsEnabled || config.widgets === "changes",
26
+ },
27
+ {
28
+ name: "host-integration",
29
+ description: "OAuth, public endpoint, stale Host metadata, and MCP App debugging.",
30
+ whenToRead: "Read for MCP connection, OAuth, deployment, or UI failures.",
31
+ },
32
+ {
33
+ name: "shell-processes",
34
+ description: "Long-running processes, write_stdin, PTY, and platform edges.",
35
+ whenToRead: "Read for running or interactive command issues.",
36
+ },
37
+ ];
38
+ function capabilityGuidesDir() {
39
+ return fileURLToPath(new URL("../capabilities", import.meta.url));
40
+ }
41
+ export function loadCapabilityGuides(config) {
42
+ const root = capabilityGuidesDir();
43
+ return CAPABILITY_GUIDE_DEFINITIONS
44
+ .filter((definition) => definition.enabled?.(config) ?? true)
45
+ .map((definition) => {
46
+ const baseDir = join(root, definition.name);
47
+ return {
48
+ name: definition.name,
49
+ description: definition.description,
50
+ whenToRead: definition.whenToRead,
51
+ baseDir,
52
+ filePath: join(baseDir, "GUIDE.md"),
53
+ };
54
+ });
55
+ }
56
+ export function resolveCapabilityGuideReadPath(guides, activatedGuideDirs, inputPath) {
57
+ const resolution = resolveAdvertisedFileReadPath(guides, activatedGuideDirs, inputPath);
58
+ if (!resolution)
59
+ return undefined;
60
+ return {
61
+ absolutePath: resolution.absolutePath,
62
+ guide: resolution.source,
63
+ isGuideFile: resolution.isEntryFile,
64
+ };
65
+ }
66
+ export function markCapabilityGuideActivated(activatedGuideDirs, guide) {
67
+ markAdvertisedFileSourceActivated(activatedGuideDirs, guide);
68
+ }
69
+ export function buildCapabilityFingerprint(config, version, context = {}) {
70
+ const capabilities = [
71
+ "workspace.close",
72
+ "worktree.managed",
73
+ "filesystem.rename-move",
74
+ "filesystem.delete",
75
+ "process.write-stdin",
76
+ "hooks.lifecycle",
77
+ "capability-guides.read",
78
+ ];
79
+ if (config.toolMode === "full") {
80
+ capabilities.push("inspection.search-tools");
81
+ }
82
+ if (config.subagents) {
83
+ capabilities.push("subagent.profiles");
84
+ }
85
+ if (config.artifactsEnabled && context.artifactDownloadSupported) {
86
+ capabilities.push("artifact.native-download");
87
+ }
88
+ if (config.widgets !== "off") {
89
+ capabilities.push("ui.mcp-app");
90
+ }
91
+ if (config.widgets === "changes") {
92
+ capabilities.push("review.show-changes");
93
+ }
94
+ return {
95
+ version,
96
+ toolMode: config.toolMode,
97
+ capabilities,
98
+ };
99
+ }
package/dist/cli.js CHANGED
@@ -18,7 +18,7 @@ import { isLocalAgentProvider, loadLocalAgentProfiles, } from "./local-agent-pro
18
18
  import { assertLocalAgentProviderAvailable, formatLocalAgentProviderAvailabilitySummary, } from "./local-agent-availability.js";
19
19
  import { formatAvailableLocalAgentTargets, parseLocalAgentRunArgs, resolveLocalAgentTarget, } from "./local-agent-targets.js";
20
20
  import { createLocalAgentStore } from "./local-agent-store.js";
21
- import { ensureDevspaceDefaultSkills, generateOwnerToken, loadDevspaceFiles, resolveSubagentsFlag, writeDevspaceAuth, writeDevspaceConfig, } from "./user-config.js";
21
+ import { generateOwnerToken, loadDevspaceFiles, resolveSubagentsFlag, writeDevspaceAuth, writeDevspaceConfig, } from "./user-config.js";
22
22
  import { expandHomePath } from "./roots.js";
23
23
  import { shutdownHttpServer } from "./server-shutdown.js";
24
24
  const require = createRequire(import.meta.url);
@@ -141,11 +141,9 @@ async function runInit({ force }) {
141
141
  };
142
142
  const configPath = writeDevspaceConfig(config);
143
143
  const authPath = writeDevspaceAuth(auth);
144
- const seededSkillPaths = config.subagents ? ensureDevspaceDefaultSkills() : [];
145
144
  const lines = [
146
145
  `Config: ${configPath}`,
147
146
  `Auth: ${authPath}`,
148
- ...seededSkillPaths.map((path) => `Default skill: ${path}`),
149
147
  `Local MCP URL: http://${config.host}:${config.port}/mcp`,
150
148
  ...(publicBaseUrl ? [`Public MCP URL: ${publicBaseUrl}/mcp`] : []),
151
149
  ];
package/dist/config.js CHANGED
@@ -109,11 +109,12 @@ function parsePositiveInteger(value, fallback, name, max = Number.MAX_SAFE_INTEG
109
109
  }
110
110
  return parsed;
111
111
  }
112
- function parseLoggingConfig(env) {
112
+ function parseLoggingConfig(env, trustProxyDefault) {
113
113
  const format = parseLogFormat(productEnv(env, "LOG_FORMAT"));
114
114
  const requests = productEnv(env, "LOG_REQUESTS");
115
115
  const toolCalls = productEnv(env, "LOG_TOOL_CALLS");
116
116
  const shellCommands = productEnv(env, "LOG_SHELL_COMMANDS");
117
+ const trustProxy = productEnv(env, "TRUST_PROXY");
117
118
  return {
118
119
  level: parseLogLevel(productEnv(env, "LOG_LEVEL")),
119
120
  format,
@@ -121,9 +122,16 @@ function parseLoggingConfig(env) {
121
122
  assets: parseBoolean(productEnv(env, "LOG_ASSETS")),
122
123
  toolCalls: toolCalls === undefined ? true : parseBoolean(toolCalls),
123
124
  shellCommands: shellCommands === undefined ? format === "pretty" : parseBoolean(shellCommands),
124
- trustProxy: parseBoolean(productEnv(env, "TRUST_PROXY")),
125
+ trustProxy: trustProxy === undefined ? trustProxyDefault : parseBoolean(trustProxy),
125
126
  };
126
127
  }
128
+ function shouldTrustOneProxyByDefault(host, publicBaseUrl) {
129
+ return isLoopbackHost(host) && !isLoopbackHost(new URL(publicBaseUrl).hostname);
130
+ }
131
+ function isLoopbackHost(host) {
132
+ const normalized = host.trim().toLowerCase().replace(/^\[(.*)\]$/, "$1");
133
+ return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1";
134
+ }
127
135
  function parseWidgetMode(value) {
128
136
  if (!value || value === "full")
129
137
  return "full";
@@ -218,7 +226,7 @@ export function loadConfig(env = process.env) {
218
226
  agentDir: resolve(expandHomePath(productEnv(env, "AGENT_DIR") ?? files.config.agentDir ?? defaultAgentDir())),
219
227
  systemInstructionsPath: parseSystemInstructionsPath(productEnv(env, "SYSTEM_INSTRUCTIONS_PATH") ?? files.config.systemInstructionsPath),
220
228
  hooks: mergeHookConfigs(parseHookConfig(files.config.hooks), parseHookConfig(files.hooks), files.hookFiles),
221
- logging: parseLoggingConfig(env),
229
+ logging: parseLoggingConfig(env, shouldTrustOneProxyByDefault(host, publicBaseUrl)),
222
230
  };
223
231
  }
224
232
  function numberConfigValue(value) {
package/dist/logger.js CHANGED
@@ -47,15 +47,7 @@ export function logEvent(config, level, event, fields = {}) {
47
47
  console.log(line);
48
48
  }
49
49
  }
50
- export function requestIp(req, trustProxy) {
51
- if (trustProxy) {
52
- const cfConnectingIp = firstHeaderValue(req.header("cf-connecting-ip"));
53
- if (cfConnectingIp)
54
- return cfConnectingIp;
55
- const forwardedFor = firstHeaderValue(req.header("x-forwarded-for"));
56
- if (forwardedFor)
57
- return forwardedFor;
58
- }
50
+ export function requestIp(req) {
59
51
  return req.ip ?? req.socket.remoteAddress;
60
52
  }
61
53
  export function requestPath(req) {
@@ -94,9 +86,6 @@ export function formatPrettyLogEntry(entry, options = {}) {
94
86
  ].filter((value) => Boolean(value)).join(" ");
95
87
  return `${prefix} ${formatPrettyMessage(entry, options)}`;
96
88
  }
97
- function firstHeaderValue(value) {
98
- return value?.split(",")[0]?.trim() || undefined;
99
- }
100
89
  function formatPrettyMessage(entry, options) {
101
90
  switch (String(entry.event)) {
102
91
  case "tool_call":