@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.
@@ -26,6 +26,7 @@ npx @akira-tl/forgerelay serve
26
26
  npx @akira-tl/forgerelay doctor
27
27
  npx @akira-tl/forgerelay config get
28
28
  npx @akira-tl/forgerelay config set publicBaseUrl https://forge.example.com/forgerelay/main,https://forge-alt.example.com/relay
29
+ npx @akira-tl/forgerelay maintenance inspect
29
30
  ```
30
31
 
31
32
  ## Environment variables
@@ -46,6 +47,73 @@ The public environment-variable prefix is `FORGERELAY_*`.
46
47
  | `FORGERELAY_WORKTREE_ROOT` | Managed worktree directory. New default: `~/.forgerelay/worktrees`. |
47
48
  | `FORGERELAY_WORKFLOW_INSTRUCTIONS` | Replace the built-in workflow policy while retaining the capability contract. |
48
49
  | `FORGERELAY_APPEND_INSTRUCTIONS` | Append project/operator workflow policy. |
50
+ | `FORGERELAY_RETENTION_HISTORY_DAYS` | Optional owner-authorized age window for related Activity/Audit, Host Turn, and durable Bash history. Unset means unlimited retention. |
51
+ | `FORGERELAY_RETENTION_ORPHANED_ADMIN` | Optional boolean authorization for provably orphaned/rebuildable administrative state. Unset/false means do not reclaim it. |
52
+
53
+ ### Retention inspection and prune
54
+
55
+ Durable ForgeRelay history is retained without an age limit by default. The owner can
56
+ inspect what is retained, protected, and potentially reclaimable without starting the
57
+ server:
58
+
59
+ ```bash
60
+ npx @akira-tl/forgerelay maintenance inspect
61
+ npx @akira-tl/forgerelay maintenance inspect --json
62
+ ```
63
+
64
+ Inspection is read-only: it snapshots an existing SQLite database and WAL into a
65
+ temporary directory before opening that snapshot read-only, reads bounded
66
+ Workspace-private Task/checkpoint metadata, and uses read-only Git ref/worktree
67
+ queries. It does not create or migrate a missing/older source database, touch Workspace
68
+ last-used timestamps, move refs, or alter worktrees.
69
+
70
+ After reviewing that report, the owner can explicitly apply the configured policy:
71
+
72
+ ```bash
73
+ npx @akira-tl/forgerelay maintenance prune
74
+ npx @akira-tl/forgerelay maintenance prune --json
75
+ ```
76
+
77
+ `maintenance prune` is manual and policy-gated. With neither `historyDays` nor
78
+ `orphanedAdministrativeState` authorized it is a no-op. It acquires the ForgeRelay
79
+ runtime lease before any destructive work, so it refuses to run concurrently with a
80
+ server using the same state directory. Historical cleanup is performed in whole Host
81
+ Turn cohorts: a turn is retained if any Activity is recent, nonterminal, owns a running
82
+ Bash stream, or is tied to an active Subagent Run. When removed Activity payloads share
83
+ a segmented log with retained payloads, retained bytes are compacted to a new segment
84
+ before the old unreferenced segment is deleted.
85
+
86
+ Administrative cleanup is deliberately conservative. It removes only review refs that
87
+ can be proven orphaned and rebuildable, plus empty orphan Workspace-state directories.
88
+ Canonical and alias Workspace identities, any non-empty private Workspace state,
89
+ Workspace Tasks, named checkpoints, active/runtime state, managed worktrees, and
90
+ managed branches remain protected. Repeating the same prune is idempotent and should
91
+ report no newly eligible state after the first successful pass.
92
+
93
+ The persisted policy shape is:
94
+
95
+ ```json
96
+ {
97
+ "retention": {
98
+ "historyDays": 30,
99
+ "orphanedAdministrativeState": false
100
+ }
101
+ }
102
+ ```
103
+
104
+ `historyDays` is one shared cutoff for related Activity/Audit, Host Turn, and durable
105
+ Bash history so later owner-authorized maintenance can preserve cross-store
106
+ consistency. Omit it for unlimited durable-history retention. The environment override
107
+ is `FORGERELAY_RETENTION_HISTORY_DAYS`. `orphanedAdministrativeState` is a separate
108
+ explicit authorization, with `FORGERELAY_RETENTION_ORPHANED_ADMIN` as its environment
109
+ override.
110
+
111
+ Named Workspace checkpoints and Workspace Tasks are protected by this policy:
112
+ checkpoint removal remains an explicit checkpoint operation, and retention maintenance
113
+ never treats Tasks as disposable history. Existing automatic runtime GC is separate:
114
+ it bounds rebuildable/in-memory runtime resources and expires stale context-delivery
115
+ bookkeeping; it does **not** age-prune durable Activity/Audit history, durable Bash
116
+ output, named checkpoints, persistent Workspace identity, or Task Lists.
49
117
 
50
118
  ### Routed and multi-origin public deployments
51
119
 
@@ -61,12 +129,18 @@ or multiple URLs as an array:
61
129
  }
62
130
  ```
63
131
 
64
- Each entry keeps its own route prefix. The first URL is canonical and is used for
65
- generated OAuth/MCP URLs; every configured hostname is included in the derived
66
- Host-header allowlist. MCP App `_meta.ui.domain` uses the canonical URL's origin,
67
- while CSP resource/connect entries include every full public base URL. The full
68
- ordered `publicBaseUrl` list also participates in the MCP App resource cache
69
- identity, so changing any domain or route produces a new `ui://` resource URI.
132
+ Each entry keeps its own route prefix. Every configured pathname is an accepted
133
+ inbound operational route boundary; the first URL remains canonical for generated
134
+ OAuth/MCP metadata and links. For example, if the only configured URL is
135
+ `https://forge.example.com/forgerelay/main`, MCP, OAuth operations, health, and MCP App
136
+ assets are served below `/forgerelay/main/*`; naked `/mcp`, `/authorize`, `/token`,
137
+ `/healthz`, and `/mcp-app-assets/*` are not parallel deployment routes. Standards-based
138
+ OAuth/MCP discovery metadata remains under its required `/.well-known/...` paths.
139
+ Every configured hostname is included in the derived Host-header allowlist. MCP App
140
+ `_meta.ui.domain` uses the canonical URL's origin, while CSP resource/connect entries
141
+ include every full public base URL. The full ordered `publicBaseUrl` list also
142
+ participates in the MCP App resource cache identity, so changing any domain or route
143
+ produces a new `ui://` resource URI.
70
144
 
71
145
  A single persisted string remains fully supported, so existing configs require no
72
146
  migration. For environment configuration, use a comma-separated list in
@@ -632,7 +706,8 @@ from the normal Skill paths; ForgeRelay does not reserve, delete, or rewrite it.
632
706
  | `FORGERELAY_LOG_ASSETS` | `0` |
633
707
  | `FORGERELAY_LOG_TOOL_CALLS` | `1` |
634
708
  | `FORGERELAY_LOG_SHELL_COMMANDS` | `1` in `pretty`, `0` in `json` |
635
- | `FORGERELAY_TRUST_PROXY` | auto: `1` only for loopback bind + non-loopback public URL; otherwise `0` |
709
+ | `FORGERELAY_TRUST_PROXY` | legacy compatibility override; `1` is accepted only with a loopback bind |
710
+ | `FORGERELAY_TRUSTED_PROXIES` | explicit comma-separated trusted proxy IP addresses/CIDRs; unset by default |
636
711
 
637
712
  `pretty` is the human-facing local console format. It uses terminal-aware color,
638
713
  short timestamps, workspace-first context, and compact operation results while
@@ -650,12 +725,24 @@ overridden, JSON mode preserves request logging and omits shell command previews
650
725
  these format-specific defaults when set.
651
726
 
652
727
  When ForgeRelay binds to loopback (`127.0.0.1`, `::1`, or `localhost`) but is
653
- configured with a non-loopback public URL, it automatically trusts exactly one
654
- upstream proxy hop. This matches the normal tunnel/reverse-proxy topology and
655
- keeps OAuth rate limiting aligned with Express client-IP resolution. Set
656
- `FORGERELAY_TRUST_PROXY=0` to disable this inference, or `=1` to enable one-hop
657
- trust explicitly. ForgeRelay never auto-enables proxy trust when binding to
658
- `0.0.0.0` or another directly reachable interface.
728
+ configured with a non-loopback public URL, it trusts only the loopback proxy source.
729
+ This matches the normal local tunnel/reverse-proxy topology while preventing a public
730
+ or LAN client from becoming trusted merely because it supplied forwarded headers.
731
+ `forgerelay init` uses this model for **HTTPS reverse proxy / tunnel** mode and binds
732
+ that mode to `127.0.0.1`; **Direct LAN** mode binds to `0.0.0.0` and does not trust a
733
+ proxy by default.
734
+
735
+ Set `FORGERELAY_TRUST_PROXY=0` to disable inferred loopback trust. The legacy
736
+ `FORGERELAY_TRUST_PROXY=1` form is accepted only when ForgeRelay itself is bound to
737
+ loopback. For an advanced topology that intentionally combines direct LAN reachability
738
+ with a reverse proxy, list only the actual proxy source addresses or CIDRs, for example:
739
+
740
+ ```bash
741
+ FORGERELAY_TRUSTED_PROXIES="127.0.0.1,10.20.30.0/24" forgerelay serve
742
+ ```
743
+
744
+ The same list may be persisted as `trustedProxies` in `config.json`. Wildcard/global
745
+ trust is rejected; do not replace this with Express `trust proxy=true` on a LAN bind.
659
746
 
660
747
  ## Environment-only example
661
748
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akira-tl/forgerelay",
3
- "version": "0.9.2",
3
+ "version": "0.9.4",
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",
@@ -52,7 +52,7 @@
52
52
  "release:push-ready": "node scripts/release/push-ready.mjs",
53
53
  "postinstall": "node scripts/fix-node-pty-permissions.mjs",
54
54
  "start": "node dist/cli.js serve",
55
- "test": "node --test scripts/debug/runtime.test.mjs scripts/release-proof.test.mjs scripts/release/release-gate.test.mjs scripts/release/push-ready.test.mjs scripts/release/release-version.test.mjs && tsx src/mcp/oauth/router.test.ts && tsx src/workspaces/relay/auth/remote-auth-cli.test.ts && tsx src/workspaces/relay/auth/remote-ssh-auth-cli.test.ts && tsx src/workspaces/relay/tests/lifecycle.test.ts && tsx src/workspaces/relay/tests/routing.test.ts && tsx src/workspaces/relay/tests/ssh.test.ts && tsx src/workspaces/relay/tests/process.test.ts && tsx src/workspaces/relay/tests/recovery.test.ts && tsx src/workspaces/relay/tests/checkpoint.test.ts && tsx src/runtime/config/config.test.ts && tsx src/lsp/language-server-config.test.ts && tsx src/lsp/runtime/managed-language-servers.test.ts && tsx src/lsp/normalization/hover.test.ts && tsx src/lsp/references.server.test.ts && tsx src/lsp/operations/document-symbols.server.test.ts && tsx src/lsp/operations/workspace-symbols.server.test.ts && tsx src/lsp/operations/diagnostics-push.server.test.ts && tsx src/lsp/operations/diagnostics-pull.server.test.ts && tsx src/lsp/operations/request-hardening.server.test.ts && tsx src/lsp/operations/recovery.server.test.ts && tsx src/lsp/operations/lifecycle.server.test.ts && tsx src/lsp/runtime/semantic-requests.test.ts && tsx src/runtime/logging/logger.test.ts && tsx src/runtime/logging/proxy-trust.test.ts && tsx src/runtime/state/lock/file-lock.test.ts && tsx src/mcp/panel/mcp-app-template.test.ts && tsx src/mcp/hooks/hooks.test.ts && tsx src/mcp/server/core/capability-registry.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/mcp/request-meta.test.ts && tsx src/mcp/artifacts/incoming-artifacts.test.ts && tsx src/mcp/artifacts/artifact-download.test.ts && tsx src/ui/core/card-types.test.ts && tsx src/ui/activity/model.test.ts && tsx src/ui/activity/detail-card.test.ts && tsx src/ui/review/patch-display.test.ts && tsx src/ui/core/tool-display.test.ts && tsx src/mcp/filesystem/apply-patch.test.ts && tsx src/mcp/process/process-platform.test.ts && tsx src/mcp/process/process-sessions.test.ts && tsx src/mcp/server/transport/mcp-sessions.test.ts && tsx src/mcp/server/transport/server-shutdown.test.ts && tsx src/mcp/server/operations/mutation-diagnostics.test.ts && tsx src/subagents/providers/adapters/codex.test.ts && tsx src/subagents/providers/adapters/pi.test.ts && tsx src/subagents/providers/registry.test.ts && tsx src/subagents/providers/availability.test.ts && tsx src/subagents/profiles.test.ts && tsx src/subagents/cli-target.test.ts && tsx src/subagents/sessions/store.test.ts && tsx src/subagents/sessions/manager.test.ts && tsx src/subagents/sessions/mcp/capability.server.test.ts && tsx src/subagents/sessions/mcp/continuation.server.test.ts && tsx src/subagents/sessions/mcp/lifecycle.server.test.ts && tsx src/subagents/sessions/mcp/reconciliation.server.test.ts && tsx src/subagents/sessions/mcp/routing.server.test.ts && tsx src/mcp/filesystem/roots.test.ts && tsx src/mcp/filesystem/file-mutations.test.ts && tsx src/mcp/operations/edit-preflight.test.ts && tsx src/workspaces/resources/skills.test.ts && tsx src/runtime/state/db/migrations.test.ts && tsx src/workspaces/state/workspace-store.test.ts && tsx src/workspaces/tasks/workspace-tasks.test.ts && tsx src/workspaces/tasks/workspace-task-reminders.test.ts && tsx src/activity/history/audit-store.test.ts && tsx src/activity/history/bash-output-store.test.ts && tsx src/activity/runtime/lifecycle.test.ts && tsx src/activity/history/query-service.test.ts && tsx src/mcp/operations/core-operation-executor.test.ts && tsx src/mcp/operations/bulk-mutation.test.ts && tsx src/mcp/operations/batch/scheduler.test.ts && tsx src/mcp/operations/batch/executor-policy.test.ts && tsx src/workspaces.test.ts && tsx src/workspaces/conversation-checkout.test.ts && tsx src/workspaces/conversation-worktree.test.ts && tsx src/workspaces/git/worktree-recovery.test.ts && tsx src/mcp/server/workspace/workspace-inventory.test.ts && tsx src/mcp/server/workspace/workspace-recovery.test.ts && tsx src/mcp/server/workspace/workspace-checkpoint.test.ts && tsx src/workspaces/review/review-checkpoints.test.ts && tsx src/lsp/code-intelligence.server.test.ts && npm run build:app && tsx src/mcp/process/server.test.ts && tsx src/mcp/panel/server.test.ts && tsx src/mcp/server/server.test.ts && tsx src/mcp/oauth/oauth-store.test.ts && tsx src/cli/cli.test.ts",
55
+ "test": "node --test scripts/debug/runtime.test.mjs scripts/release-proof.test.mjs scripts/release/release-gate.test.mjs scripts/release/push-ready.test.mjs scripts/release/release-version.test.mjs && tsx src/mcp/oauth/router.test.ts && tsx src/workspaces/relay/auth/remote-auth-cli.test.ts && tsx src/workspaces/relay/auth/remote-ssh-auth-cli.test.ts && tsx src/workspaces/relay/tests/lifecycle.test.ts && tsx src/workspaces/relay/tests/routing.test.ts && tsx src/workspaces/relay/tests/ssh.test.ts && tsx src/workspaces/relay/tests/process.test.ts && tsx src/workspaces/relay/tests/recovery.test.ts && tsx src/workspaces/relay/tests/checkpoint.test.ts && tsx src/runtime/config/config.test.ts && tsx src/lsp/language-server-config.test.ts && tsx src/lsp/runtime/managed-language-servers.test.ts && tsx src/lsp/normalization/hover.test.ts && tsx src/lsp/references.server.test.ts && tsx src/lsp/operations/document-symbols.server.test.ts && tsx src/lsp/operations/workspace-symbols.server.test.ts && tsx src/lsp/operations/diagnostics-push.server.test.ts && tsx src/lsp/operations/diagnostics-pull.server.test.ts && tsx src/lsp/operations/request-hardening.server.test.ts && tsx src/lsp/operations/recovery.server.test.ts && tsx src/lsp/operations/lifecycle.server.test.ts && tsx src/lsp/runtime/semantic-requests.test.ts && tsx src/runtime/logging/logger.test.ts && tsx src/runtime/logging/proxy-trust.test.ts && tsx src/runtime/state/lock/file-lock.test.ts && tsx src/runtime/state/runtime-lease.test.ts && tsx src/mcp/panel/mcp-app-template.test.ts && tsx src/mcp/hooks/hooks.test.ts && tsx src/mcp/server/core/capability-registry.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/mcp/request-meta.test.ts && tsx src/mcp/artifacts/incoming-artifacts.test.ts && tsx src/mcp/artifacts/artifact-download.test.ts && tsx src/ui/core/card-types.test.ts && tsx src/ui/activity/model.test.ts && tsx src/ui/activity/detail-card.test.ts && tsx src/ui/review/patch-display.test.ts && tsx src/ui/core/tool-display.test.ts && tsx src/mcp/filesystem/apply-patch.test.ts && tsx src/mcp/process/process-platform.test.ts && tsx src/mcp/process/process-sessions.test.ts && tsx src/mcp/server/transport/mcp-sessions.test.ts && tsx src/mcp/server/transport/server-shutdown.test.ts && tsx src/mcp/server/operations/mutation-diagnostics.test.ts && tsx src/subagents/providers/adapters/codex.test.ts && tsx src/subagents/providers/adapters/pi.test.ts && tsx src/subagents/providers/registry.test.ts && tsx src/subagents/providers/availability.test.ts && tsx src/subagents/profiles.test.ts && tsx src/subagents/cli-target.test.ts && tsx src/subagents/sessions/store.test.ts && tsx src/subagents/sessions/manager.test.ts && tsx src/subagents/sessions/mcp/capability.server.test.ts && tsx src/subagents/sessions/mcp/continuation.server.test.ts && tsx src/subagents/sessions/mcp/lifecycle.server.test.ts && tsx src/subagents/sessions/mcp/reconciliation.server.test.ts && tsx src/subagents/sessions/mcp/routing.server.test.ts && tsx src/mcp/filesystem/roots.test.ts && tsx src/mcp/filesystem/file-mutations.test.ts && tsx src/mcp/operations/edit-preflight.test.ts && tsx src/workspaces/resources/skills.test.ts && tsx src/runtime/state/db/migrations.test.ts && tsx src/workspaces/state/workspace-store.test.ts && tsx src/workspaces/tasks/workspace-tasks.test.ts && tsx src/workspaces/tasks/workspace-task-reminders.test.ts && tsx src/activity/history/audit-store.test.ts && tsx src/activity/history/bash-output-store.test.ts && tsx src/activity/runtime/lifecycle.test.ts && tsx src/activity/history/query-service.test.ts && tsx src/mcp/operations/core-operation-executor.test.ts && tsx src/mcp/operations/bulk-mutation.test.ts && tsx src/mcp/operations/batch/scheduler.test.ts && tsx src/mcp/operations/batch/executor-policy.test.ts && tsx src/workspaces.test.ts && tsx src/workspaces/conversation-checkout.test.ts && tsx src/workspaces/conversation-worktree.test.ts && tsx src/workspaces/git/worktree-recovery.test.ts && tsx src/mcp/server/workspace/workspace-inventory.test.ts && tsx src/mcp/server/workspace/workspace-recovery.test.ts && tsx src/mcp/server/workspace/workspace-checkpoint.test.ts && tsx src/workspaces/review/review-checkpoints.test.ts && tsx src/lsp/code-intelligence.server.test.ts && npm run build:app && tsx src/mcp/process/server.test.ts && tsx src/mcp/panel/server.test.ts && tsx src/mcp/server/server.test.ts && tsx src/mcp/oauth/oauth-store.test.ts && tsx src/cli/maintenance.test.ts && tsx src/cli/maintenance-prune.test.ts && tsx src/cli/cli.test.ts",
56
56
  "typecheck": "tsc -p tsconfig.json --noEmit",
57
57
  "release:check": "node scripts/release-version.mjs check",
58
58
  "release:tag-check": "node scripts/release-version.mjs tag",
@@ -9,6 +9,13 @@ const MAX_DIRECT_FILES = 8;
9
9
  const MAX_DIRECT_DIRS = 8;
10
10
  const LINE_LIMIT_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".mjs", ".cjs", ".css"]);
11
11
 
12
+ // Append-only versioned archives grow by design and must keep their canonical
13
+ // flat paths for release tooling and stable links. They remain subject to all
14
+ // other architecture checks, including code line limits where applicable.
15
+ const DIRECT_FILE_LIMIT_EXEMPT_DIRS = new Set([
16
+ "docs/releases",
17
+ ]);
18
+
12
19
  // Repository-root protocol files are intentionally discoverable by Git, npm,
13
20
  // Node, Vite, contributors, and agents. Keep that conventional surface explicit
14
21
  // instead of hiding required/default-discovery files merely to satisfy a count.
@@ -67,7 +74,7 @@ const directories = new Set([...directFiles.keys(), ...directDirs.keys()]);
67
74
  for (const directory of [...directories].sort()) {
68
75
  const files = directFiles.get(directory) ?? 0;
69
76
  const dirs = directDirs.get(directory)?.size ?? 0;
70
- if (directory !== "." && files > MAX_DIRECT_FILES) {
77
+ if (directory !== "." && !DIRECT_FILE_LIMIT_EXEMPT_DIRS.has(directory) && files > MAX_DIRECT_FILES) {
71
78
  violations.push(`${directory}: ${files} direct files > ${MAX_DIRECT_FILES}`);
72
79
  }
73
80
  if (dirs > MAX_DIRECT_DIRS) {
@@ -83,7 +90,7 @@ if (violations.length > 0) {
83
90
 
84
91
  console.log(
85
92
  `Architecture check passed: ${tracked.length} tracked files; ` +
86
- `code <= ${MAX_LINES} lines; non-root directories <= ${MAX_DIRECT_FILES} files / ${MAX_DIRECT_DIRS} directories.`,
93
+ `code <= ${MAX_LINES} lines; bounded non-root directories <= ${MAX_DIRECT_FILES} files / ${MAX_DIRECT_DIRS} directories.`,
87
94
  );
88
95
 
89
96
  function gitTrackedFiles() {
@@ -60,6 +60,12 @@ test("cross-platform cloud CI delegates to one shell-free verification entrypoin
60
60
  }
61
61
  });
62
62
 
63
+ test("architecture gate treats the append-only release-note archive as an explicit flat-path exception", async () => {
64
+ const architecture = await readFile(resolve(repoRoot, "scripts/ci/architecture.mjs"), "utf8");
65
+ assert.match(architecture, /DIRECT_FILE_LIMIT_EXEMPT_DIRS/);
66
+ assert.match(architecture, /"docs\/releases"/);
67
+ });
68
+
63
69
  test("release runtime and local parity share the checked-in Node contract", async () => {
64
70
  const nodeVersion = (await readFile(resolve(repoRoot, ".nvmrc"), "utf8")).trim();
65
71
  assert.equal(nodeVersion, "22.19.0");