@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 +17 -0
- package/README.md +9 -3
- package/capabilities/workspace/workspace-checkpoints/GUIDE.md +35 -4
- package/dist/cli/init.js +39 -23
- package/dist/cli/maintenance-prune.js +479 -0
- package/dist/cli/maintenance-retention.js +93 -0
- package/dist/cli/maintenance.js +598 -0
- package/dist/cli/setup-support.js +21 -0
- package/dist/cli.js +29 -6
- package/dist/mcp/oauth/public-url.js +3 -0
- package/dist/mcp/oauth/router.js +14 -9
- package/dist/mcp/server/core/capabilities/workspace-checkpoint.js +9 -0
- package/dist/mcp/server/core/capability-registry.js +1 -1
- package/dist/mcp/server/transport/http-server.js +13 -8
- package/dist/runtime/config/config.js +54 -6
- package/dist/runtime/state/runtime-lease.js +109 -0
- package/dist/server.js +6 -0
- package/dist/workspaces/state/workspace-checkpoints.js +129 -13
- package/docs/configuration.md +100 -13
- package/package.json +2 -2
- package/scripts/ci/architecture.mjs +9 -2
- package/scripts/release/release-gate.test.mjs +6 -0
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.
|
|
59
|
-
|
|
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.
|
|
5
|
+
## v0.9.3 surface
|
|
6
6
|
|
|
7
|
-
Supported operations are
|
|
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,
|
|
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
|
|
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,
|
|
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")
|
|
40
|
-
? "
|
|
41
|
-
: "
|
|
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: "
|
|
58
|
-
label: "LAN
|
|
59
|
-
hint: "
|
|
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
|
-
|
|
73
|
+
const host = setupBindAddress(networkMode);
|
|
67
74
|
let publicBaseUrl = null;
|
|
68
75
|
let clientFacingBaseUrls = [`http://127.0.0.1:${port}`];
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
:
|
|
84
|
-
|
|
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:
|
|
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),
|