@akira-tl/forgerelay 0.9.2 → 0.9.4

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,23 @@ All notable ForgeRelay changes are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.4] - 2026-09-04
8
+
9
+ ### Added
10
+
11
+ - Added owner-facing `forgerelay maintenance inspect` and `forgerelay maintenance prune` for the 0.9.4 retention contract. Durable Activity/Audit, Host Turn, and Bash history remains unlimited by default; configured retention is explicit prune authorization, whole Host Turn cohorts protect recent/nonterminal/running Bash/active Subagent work, and persistent Workspace identity, aliases, Task Lists, named checkpoints, managed worktrees/branches, and non-empty private Workspace state remain protected.
12
+ - Added a runtime state lease so `serve` and destructive maintenance cannot operate on the same state directory concurrently. Retained Activity payloads sharing segments with pruned history are compacted before unreferenced old segments are removed, and repeat prune is idempotent.
13
+
14
+ ### Fixed
15
+
16
+ - Made routed `publicBaseUrl` path prefixes real inbound MCP/OAuth/health/App deployment boundaries, split setup network modes into safe loopback proxy versus direct LAN binds, and replaced permissive proxy trust with explicit trusted proxy sources. This fixes reverse-proxy `X-Forwarded-For` failures without enabling spoofable global `trust proxy` behavior.
17
+
18
+ ## [0.9.3] - 2026-09-04
19
+
20
+ ### Added
21
+
22
+ - Extended `workspace.checkpoint` with `restore.preflight` and optimistic-concurrency-protected `restore`. Restore uses deterministic Git-visible working-tree snapshot identities, refuses stale preflight tokens before mutation, restores content without moving branch HEAD or rewriting history, leaves ignored files outside the restore model, does not reconstruct staged-versus-unstaged state, remains independent from managed-worktree recovery, and routes through the owning Execution ForgeRelay / explicit Composite member.
23
+
7
24
  ## [0.9.2] - 2026-09-04
8
25
 
9
26
  ### Added
package/README.md CHANGED
@@ -55,8 +55,10 @@ http://127.0.0.1:7676/mcp
55
55
 
56
56
  If the MCP host cannot reach localhost, put ForgeRelay behind a public HTTPS
57
57
  tunnel or reverse proxy such as Cloudflare Tunnel, ngrok, Pinggy, Tailscale
58
- Funnel, or your own proxy. During setup, enter the public base URL before the
59
- final `/mcp`; routed prefixes are allowed:
58
+ Funnel, or your own proxy. Setup separates **Direct LAN** (`0.0.0.0`) from
59
+ **HTTPS reverse proxy / tunnel** (`127.0.0.1` with loopback-only proxy trust), so
60
+ you do not need to choose a bind address manually. During setup, enter the public
61
+ base URL before the final `/mcp`; routed prefixes are allowed:
60
62
 
61
63
  ```text
62
64
  https://your-tunnel-host.example.com/forgerelay/main
@@ -69,7 +71,11 @@ https://your-tunnel-host.example.com/forgerelay/main/mcp
69
71
  ```
70
72
 
71
73
  `publicBaseUrl` may also be an ordered list when multiple public entries are
72
- valid; each entry may use its own route and the first is canonical.
74
+ valid; each entry may use its own route and the first is canonical for generated
75
+ metadata/links. Every configured pathname is an accepted inbound operational route
76
+ boundary. If the only configured route is `/forgerelay/main`, MCP/OAuth/health/App
77
+ routes are served below that prefix rather than in parallel at naked `/mcp`,
78
+ `/authorize`, or `/healthz` paths.
73
79
 
74
80
  ForgeRelay uses an Owner-password OAuth approval flow. `forgerelay init` prints
75
81
  the password and stores it in the active config directory. New installations use:
@@ -2,16 +2,18 @@
2
2
 
3
3
  `workspace.checkpoint` provides low-frequency, persistent checkpoints owned by the current filesystem Workspace. Checkpoints are immutable Git-backed snapshots intended for deliberate recovery/history workflows.
4
4
 
5
- ## v0.9.2 surface
5
+ ## v0.9.3 surface
6
6
 
7
- Supported operations are deliberately limited to:
7
+ Supported operations are:
8
8
 
9
9
  - `create` — create a named immutable checkpoint of the current Git-visible working tree.
10
- - `list` — return bounded checkpoint metadata, newest identity preserved in creation order.
10
+ - `list` — return bounded checkpoint metadata, preserving checkpoint creation order.
11
11
  - `inspect` — return bounded metadata for one checkpoint.
12
+ - `restore.preflight` — identify the selected checkpoint content snapshot and the current Git-visible working snapshot without mutating files.
13
+ - `restore` — restore checkpoint content only when the caller supplies the still-current snapshot identity returned by preflight.
12
14
  - `delete` — explicitly delete one checkpoint and its ForgeRelay-owned Git ref.
13
15
 
14
- Restore is **not** part of v0.9.2. Do not emulate restore with checkout/reset or other destructive Git commands unless the user separately and explicitly asks for such Git work outside this Capability.
16
+ Restore is deliberately a two-step optimistic-concurrency operation. Do not replace it with checkout/reset, hidden merge/rebase behavior, or branch-history rewrites.
15
17
 
16
18
  ## Create
17
19
 
@@ -53,6 +55,35 @@ Use `inspect` only after selecting an id:
53
55
 
54
56
  `list` and `inspect` expose bounded metadata only. Checkpoints do not move when `review.changes` advances its independent last-shown baseline.
55
57
 
58
+ ## Restore preflight and restore
59
+
60
+ First preflight the selected checkpoint:
61
+
62
+ ```json
63
+ {
64
+ "operation": "restore.preflight",
65
+ "checkpointId": "cp_0123456789"
66
+ }
67
+ ```
68
+
69
+ The result includes `checkpointSnapshot`, the immutable Git tree identity selected for restore, plus `currentSnapshot`, the deterministic Git tree identity of the current Git-visible working content. It also includes a bounded `restoreSummary`. Preflight does not mutate the Workspace.
70
+
71
+ Then pass that exact `currentSnapshot` back as `expectedCurrentSnapshot`:
72
+
73
+ ```json
74
+ {
75
+ "operation": "restore",
76
+ "checkpointId": "cp_0123456789",
77
+ "expectedCurrentSnapshot": "0123456789abcdef0123456789abcdef01234567"
78
+ }
79
+ ```
80
+
81
+ Immediately before applying content changes ForgeRelay recomputes the current Git-visible working snapshot. If it no longer matches `expectedCurrentSnapshot`, restore fails before mutation and the intervening edits remain untouched. Run preflight again before deciding whether to retry.
82
+
83
+ A successful restore writes only ordinary working-tree content needed to reproduce the checkpoint's Git-visible tree. It does **not** move branch `HEAD`, rewrite commit history, merge/rebase, auto-commit, or use `git reset --hard` semantics. Ignored files remain outside the checkpoint/restore content model and are not intentionally changed.
84
+
85
+ The 0.9 restore contract is **content-state only**. ForgeRelay does not reconstruct or promise the historical staged-versus-unstaged partition. Restore does not rewrite the real Git index; existing staging state may therefore differ from the restored working-tree content and should be inspected normally with Git afterward. Results expose `stagingStateRestored: false` to make this explicit.
86
+
56
87
  ## Delete
57
88
 
58
89
  Deletion is explicit:
package/dist/cli/init.js CHANGED
@@ -4,7 +4,7 @@ import { publicEndpointUrl } from "../mcp/oauth/public-url.js";
4
4
  import { expandHomePath } from "../mcp/filesystem/roots.js";
5
5
  import { installManagedLanguageServers, installedManagedLanguageServers, managedLanguageServerOptions, } from "../lsp/runtime/managed-language-servers.js";
6
6
  import { generateInstanceId, generateOwnerToken, loadForgeRelayFiles, resolveSubagentsFlag, writeForgeRelayAuth, writeForgeRelayConfig, } from "../runtime/config/user-config.js";
7
- import { classifyClientFacingBaseUrl, compactPublicBaseUrlConfig, hasInsecureLanBaseUrl, isLoopbackBindAddress, normalizePublicBaseUrlsInput, SetupCancelledError, textPrompt, validateBindAddress, validateClientFacingBaseUrls, validatePort, } from "./setup-support.js";
7
+ import { classifyClientFacingBaseUrl, compactPublicBaseUrlConfig, hasInsecureLanBaseUrl, isLoopbackBindAddress, normalizePublicBaseUrlsInput, setupBindAddress, SetupCancelledError, textPrompt, validateHttpsProxyBaseUrls, validateLanClientFacingBaseUrls, validatePort, } from "./setup-support.js";
8
8
  export async function runInit({ force }) {
9
9
  const files = loadForgeRelayFiles();
10
10
  if (!force && files.configExists && files.authExists) {
@@ -36,9 +36,11 @@ export async function runInit({ force }) {
36
36
  const existingPublicBaseUrls = Array.isArray(files.config.publicBaseUrl)
37
37
  ? files.config.publicBaseUrl
38
38
  : files.config.publicBaseUrl ? [files.config.publicBaseUrl] : [];
39
- const defaultNetworkMode = !isLoopbackBindAddress(files.config.host ?? "127.0.0.1") || existingPublicBaseUrls.length > 0
40
- ? "network"
41
- : "local";
39
+ const defaultNetworkMode = !isLoopbackBindAddress(files.config.host ?? "127.0.0.1")
40
+ ? "lan"
41
+ : existingPublicBaseUrls.some((baseUrl) => new URL(baseUrl).protocol === "https:")
42
+ ? "proxy"
43
+ : "local";
42
44
  const selectedMode = await prompts.select({
43
45
  message: "How should clients reach this ForgeRelay instance?",
44
46
  initialValue: defaultNetworkMode,
@@ -54,40 +56,53 @@ export async function runInit({ force }) {
54
56
  hint: "Bind to loopback; another ForgeRelay reaches it through an SSH tunnel.",
55
57
  },
56
58
  {
57
- value: "network",
58
- label: "LAN / HTTPS proxy",
59
- hint: "Expose through a LAN address or an HTTPS reverse proxy/tunnel.",
59
+ value: "lan",
60
+ label: "Direct LAN",
61
+ hint: "Bind to 0.0.0.0; clients connect directly over a trusted private LAN.",
62
+ },
63
+ {
64
+ value: "proxy",
65
+ label: "HTTPS reverse proxy / tunnel",
66
+ hint: "Bind to 127.0.0.1; a local trusted proxy publishes the HTTPS endpoint.",
60
67
  },
61
68
  ],
62
69
  });
63
70
  if (prompts.isCancel(selectedMode))
64
71
  throw new SetupCancelledError();
65
72
  const networkMode = selectedMode;
66
- let host = "127.0.0.1";
73
+ const host = setupBindAddress(networkMode);
67
74
  let publicBaseUrl = null;
68
75
  let clientFacingBaseUrls = [`http://127.0.0.1:${port}`];
69
- if (networkMode === "network") {
70
- const defaultHost = files.config.host && !isLoopbackBindAddress(files.config.host)
71
- ? files.config.host
72
- : "0.0.0.0";
73
- host = await textPrompt({
74
- message: "Which address should ForgeRelay bind to? Use 0.0.0.0 for direct LAN, or 127.0.0.1 behind a local reverse proxy.",
75
- placeholder: defaultHost,
76
- defaultValue: defaultHost,
77
- validate: validateBindAddress,
78
- });
79
- const defaultClientFacing = existingPublicBaseUrls.join(", ");
76
+ const trustedProxies = networkMode === "proxy" ? ["loopback"] : undefined;
77
+ if (networkMode === "lan" || networkMode === "proxy") {
78
+ const validateBaseUrls = networkMode === "lan"
79
+ ? validateLanClientFacingBaseUrls
80
+ : validateHttpsProxyBaseUrls;
81
+ const existingClientFacing = existingPublicBaseUrls.join(", ");
82
+ const defaultClientFacing = existingClientFacing && validateBaseUrls(existingClientFacing) === undefined
83
+ ? existingClientFacing
84
+ : "";
85
+ if (networkMode === "proxy") {
86
+ prompts.note([
87
+ "The public URL may include a path prefix, for example https://example.com/forgerelay/debug.",
88
+ "That prefix is the deployment route boundary, so MCP, OAuth, health, and App assets are served below it.",
89
+ ].join("\n"), "Routed public URL");
90
+ }
80
91
  clientFacingBaseUrls = normalizePublicBaseUrlsInput(await textPrompt({
81
92
  message: defaultClientFacing
82
93
  ? `What client-facing base URLs should clients use? Press Enter to keep ${defaultClientFacing}`
83
- : "What client-facing base URL should clients use?",
84
- placeholder: defaultClientFacing || `http://192.168.1.20:${port} or https://forge.example.com`,
94
+ : networkMode === "lan"
95
+ ? "What direct LAN base URL should clients use?"
96
+ : "What HTTPS public base URL should clients use?",
97
+ placeholder: defaultClientFacing || (networkMode === "lan"
98
+ ? `http://192.168.1.20:${port}`
99
+ : "https://example.com/forgerelay/debug"),
85
100
  defaultValue: defaultClientFacing,
86
- validate: validateClientFacingBaseUrls,
101
+ validate: validateBaseUrls,
87
102
  }));
88
103
  for (const baseUrl of clientFacingBaseUrls)
89
104
  classifyClientFacingBaseUrl(baseUrl);
90
- if (hasInsecureLanBaseUrl(clientFacingBaseUrls)) {
105
+ if (networkMode === "lan" && hasInsecureLanBaseUrl(clientFacingBaseUrls)) {
91
106
  prompts.note([
92
107
  "Plain HTTP does not encrypt the ForgeRelay Owner approval flow or MCP bearer tokens.",
93
108
  "Use this only on a trusted private LAN. Prefer SSH relay or HTTPS when the network is not fully trusted.",
@@ -140,6 +155,7 @@ export async function runInit({ force }) {
140
155
  allowedRoots,
141
156
  publicBaseUrl,
142
157
  allowedHosts: files.config.allowedHosts,
158
+ trustedProxies,
143
159
  workflowInstructions: files.config.workflowInstructions,
144
160
  appendInstructions: files.config.appendInstructions,
145
161
  subagents: resolveSubagentsFlag(files.config),