@akira-tl/forgerelay 1.2.0 → 1.2.2

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,25 @@ All notable ForgeRelay changes are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.2.2] - 2026-09-13
8
+
9
+ ### Fixed
10
+
11
+ - 修复带路径前缀的 `publicBaseUrl` 在 MCP SDK v2 下被 Origin 校验错误拒绝的问题;ForgeRelay 现在只从 canonical public URL 派生允许的 Origin hostname,同时继续拒绝 foreign / malformed Origin。
12
+ - OAuth authorization callback 现在带回 metadata 中公布的 `iss`,使 routed public URL 的现代 OAuth 流程保持 issuer 一致。
13
+ - MCP App 的 CSP `resourceDomains` / `connectDomains` 现在使用 URL Origin,而不是包含部署路径的完整 `publicBaseUrl`,避免 path-prefix 部署下 Activity Panel 只停留在 `Waiting for Activity Panel state.`。
14
+ - Skill 默认发现范围收敛为 Project `.agents/skills`、Project `.forgerelay/skills` 与 active ForgeRelay config `skills`;不再自动扫描全局 `~/.agents/skills` 或 `FORGERELAY_AGENT_DIR/skills`,显式 `FORGERELAY_SKILL_PATHS` 仍作为用户附加来源。
15
+
16
+ ## [1.2.1] - 2026-09-13
17
+
18
+ ### Fixed
19
+
20
+ - Reconstruct relayed and Composite Workspace Panel presentation from durable Workspace state across modern/stateless MCP requests, preventing the ChatGPT Activity Panel from remaining on `Waiting...` after a relayed `open_workspace`; closed Workspaces remain non-renderable.
21
+
22
+ ### Changed
23
+
24
+ - Added Composite + Relay execution-context coverage to the authoritative `workspace-mcp` test shard, with Shell Instructions enabled explicitly where that opt-in behavior is under test.
25
+
7
26
  ## [1.2.0] - 2026-09-13
8
27
 
9
28
  ### Changed
@@ -56,7 +56,7 @@ export function createForgeRelayAuthRouter(options) {
56
56
  res.status(200).json({ ...tokens, ...(instanceId ? { instance_id: instanceId } : {}) });
57
57
  });
58
58
  }
59
- router.use(authorizationPaths, authorizationHandler({ provider }));
59
+ router.use(authorizationPaths, authorizationHandler({ provider, issuerUrl }));
60
60
  router.use(tokenPaths, tokenHandler({ provider }));
61
61
  if (provider.clientsStore.registerClient && registrationEndpoint) {
62
62
  router.use(registrationPaths, clientRegistrationHandler({ clientsStore: provider.clientsStore }));
@@ -64,9 +64,10 @@ function currentIdentity(config, fallbackRevision) {
64
64
  return identity;
65
65
  }
66
66
  function activityPanelCsp(config) {
67
+ const publicOrigins = Array.from(new Set(config.publicBaseUrls.map((baseUrl) => new URL(baseUrl).origin)));
67
68
  return {
68
- resourceDomains: [...config.publicBaseUrls],
69
- connectDomains: [...config.publicBaseUrls],
69
+ resourceDomains: publicOrigins,
70
+ connectDomains: publicOrigins,
70
71
  };
71
72
  }
72
73
  async function readActivityPanelAppResource(config, fallbackRevision, requestedUri, transportSessionId) {
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { checkResourceAllowed, createMcpHandler, isInitializeRequest, isLegacyRequest, resourceUrlFromServerUrl, } from "@modelcontextprotocol/server";
2
+ import { checkResourceAllowed, createMcpHandler, isInitializeRequest, isLegacyRequest, localhostAllowedOrigins, resourceUrlFromServerUrl, } from "@modelcontextprotocol/server";
3
3
  import { createMcpExpressApp, getOAuthProtectedResourceMetadataUrl, requireBearerAuth, } from "@modelcontextprotocol/express";
4
4
  import { NodeStreamableHTTPServerTransport, toNodeHandler, toWebRequest, } from "@modelcontextprotocol/node";
5
5
  import express from "express";
@@ -32,17 +32,22 @@ const MCP_TRANSPORT_CLEANUP_INTERVAL_MS = 5 * 60 * 1_000;
32
32
  export function createHttpServer(config, options, createMcpServer) {
33
33
  const incomingArtifactAdapters = options.incomingArtifactAdapters
34
34
  ?? [createOpenAIIncomingArtifactAdapter()];
35
+ const routeBaseUrls = config.publicBaseUrls.map((baseUrl) => new URL(baseUrl));
35
36
  const allowedHosts = config.allowedHosts.includes("*")
36
37
  ? undefined
37
38
  : Array.from(new Set([config.host, ...config.allowedHosts]));
39
+ const allowedOrigins = Array.from(new Set([
40
+ ...localhostAllowedOrigins(),
41
+ ...routeBaseUrls.map((baseUrl) => baseUrl.hostname),
42
+ ]));
38
43
  const app = createMcpExpressApp({
39
44
  host: config.host,
40
45
  ...(allowedHosts ? { allowedHosts } : {}),
46
+ allowedOrigins,
41
47
  });
42
48
  const transports = new McpTransportRegistry({
43
49
  maxTransports: MAX_MCP_TRANSPORT_SESSIONS,
44
50
  });
45
- const routeBaseUrls = config.publicBaseUrls.map((baseUrl) => new URL(baseUrl));
46
51
  const mcpUrl = publicEndpointUrl(config.publicBaseUrl, "mcp");
47
52
  const mcpPaths = publicEndpointPaths(routeBaseUrls, "mcp");
48
53
  const activityPanelAssetsPaths = publicEndpointPaths(routeBaseUrls, "mcp-app-assets");
package/dist/server.js CHANGED
@@ -30,7 +30,7 @@ import { formatPathForPrompt } from "./workspaces/resources/skills.js";
30
30
  import { WorkspaceTaskReminderTracker } from "./workspaces/tasks/workspace-task-reminders.js";
31
31
  import { WorkspaceTaskStore } from "./workspaces/tasks/workspace-tasks.js";
32
32
  import { WorkspaceCheckpointStore } from "./workspaces/state/workspace-checkpoints.js";
33
- import { compactWorkspacePresentation } from "./workspaces/presentation/workspace-presentation.js";
33
+ import { compactCompositeWorkspacePresentation, compactRelayedWorkspacePresentation, compactWorkspacePresentation, } from "./workspaces/presentation/workspace-presentation.js";
34
34
  import { formatAgentsPath } from "./workspaces.js";
35
35
  import { summarizeSubagentProfile } from "./subagents/profiles.js";
36
36
  import { formatSubagentProviderAvailabilitySummary } from "./subagents/providers/availability.js";
@@ -442,8 +442,26 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
442
442
  };
443
443
  const workspacePanelState = async (workspaceId) => {
444
444
  const remembered = workspacePanelStates.get(workspaceId);
445
- if (remoteWorkspaces.has(workspaceId) || compositeWorkspaces.has(workspaceId)) {
446
- return remembered;
445
+ if (compositeWorkspaces.has(workspaceId)) {
446
+ if (remembered)
447
+ return remembered;
448
+ const composite = compositeWorkspaces.get(workspaceId);
449
+ return composite.status === "active"
450
+ ? compactCompositeWorkspacePresentation(composite)
451
+ : undefined;
452
+ }
453
+ if (remoteWorkspaces.has(workspaceId)) {
454
+ if (remembered)
455
+ return remembered;
456
+ try {
457
+ const inspected = await remoteWorkspaces.inspectWorkspace(workspaceId);
458
+ return inspected.status === "closed" || inspected.state === "closed"
459
+ ? undefined
460
+ : compactRelayedWorkspacePresentation(inspected);
461
+ }
462
+ catch {
463
+ return undefined;
464
+ }
447
465
  }
448
466
  try {
449
467
  const workspace = workspaces.getWorkspace(workspaceId);
@@ -37,6 +37,27 @@ export function compactWorkspacePresentation(card) {
37
37
  presentation.presentationRevision = presentationRevision(card, presentation);
38
38
  return presentation;
39
39
  }
40
+ export function compactCompositeWorkspacePresentation(composite) {
41
+ return compactWorkspacePresentation({
42
+ workspaceId: composite.id,
43
+ kind: "composite",
44
+ name: composite.name,
45
+ path: composite.name,
46
+ members: composite.members,
47
+ summary: { members: composite.members.length, status: composite.status },
48
+ });
49
+ }
50
+ export function compactRelayedWorkspacePresentation(inspected) {
51
+ return compactWorkspacePresentation({
52
+ workspaceId: inspected.workspaceId,
53
+ kind: inspected.kind,
54
+ root: inspected.root,
55
+ path: inspected.root,
56
+ mode: inspected.mode,
57
+ sourceRoot: inspected.sourceRoot,
58
+ summary: { mode: inspected.mode, relay: inspected.relay },
59
+ });
60
+ }
40
61
  function projectExecutionContext(value) {
41
62
  if (!value || typeof value !== "object" || Array.isArray(value))
42
63
  return undefined;
@@ -68,6 +68,7 @@ export async function startForge(t, options) {
68
68
  ...(options.taskReminderInterval !== undefined ? { taskReminderInterval: options.taskReminderInterval } : {}),
69
69
  ...(options.mediaMaxBytes !== undefined ? { mediaMaxBytes: options.mediaMaxBytes } : {}),
70
70
  ...(options.mcpServers ? { mcpServers: options.mcpServers } : {}),
71
+ ...(options.shellInstructions !== undefined ? { shellInstructions: options.shellInstructions } : {}),
71
72
  ...(options.hooks ? { hooks: options.hooks } : {}),
72
73
  }, null, 2));
73
74
  const env = {
@@ -8,9 +8,8 @@ const FRONTMATTER_DELIMITER = "---";
8
8
  export function effectiveSkillPaths(config, cwd) {
9
9
  const defaultPathCandidates = [
10
10
  resolve(cwd, ".agents", "skills"),
11
- join(homedir(), ".agents", "skills"),
11
+ resolve(cwd, ".forgerelay", "skills"),
12
12
  config.configSkillsDir,
13
- join(config.agentDir, "skills"),
14
13
  ];
15
14
  const defaultPaths = defaultPathCandidates.filter((path) => path !== undefined && existsSync(path));
16
15
  const seen = new Set();
@@ -189,7 +189,8 @@ instructions before the requested file content. Side-effecting file tools and sh
189
189
  commands discover instructions before execution; if new local instructions are
190
190
  found, ForgeRelay returns them and requires the Agent to retry, so the side effect
191
191
  does not occur before the relevant instructions are known. `FORGERELAY_AGENT_DIR`
192
- is not an instruction source; it remains only a compatibility skill-discovery path.
192
+ is neither an instruction source nor an automatic Skill-discovery source; it remains
193
+ an integration runtime directory for supported Agent tooling.
193
194
 
194
195
  ## MCP capability loading
195
196
 
@@ -232,17 +233,16 @@ force the Host to discard a cached tool schema.
232
233
 
233
234
  ## Agent Skills
234
235
 
235
- ForgeRelay discovers standard Agent Skills in precedence order from:
236
+ ForgeRelay discovers Skills in precedence order from:
236
237
 
237
238
  - project `.agents/skills`
238
- - `~/.agents/skills`
239
- - the active ForgeRelay config directory's `skills` folder
240
- - `FORGERELAY_AGENT_DIR/skills` (defaults to `~/.codex/skills`)
241
- - paths from `FORGERELAY_SKILL_PATHS`
239
+ - project `.forgerelay/skills`
240
+ - the active ForgeRelay config directory's `skills` folder (`~/.forgerelay/skills` by default)
241
+ - paths explicitly added through `FORGERELAY_SKILL_PATHS`
242
242
 
243
- These paths are discovery sources, not one shared ownership domain. `.agents/skills` is the open Agent Skills ecosystem and may contain files or symlinks managed by other Agent tooling. ForgeRelay-owned Skills remain private to the active ForgeRelay config directory (`~/.forgerelay/skills` by default) and are never migrated into `~/.agents/skills`.
243
+ These paths are discovery sources, not one shared ownership domain. Project `.agents/skills` is the open Agent Skills ecosystem and may contain files or symlinks managed by other Agent tooling. ForgeRelay-owned project/system Skills stay under `.forgerelay/skills` and the active ForgeRelay config directory. ForgeRelay does not automatically scan global Agent runtime Skill directories such as `~/.agents/skills` or `FORGERELAY_AGENT_DIR/skills`.
244
244
 
245
- Same-named collisions use the first source, so project Skills override global Skills.
245
+ Same-named collisions use the first source: project Agent Skills override project ForgeRelay Skills, which override system ForgeRelay Skills and explicit additional paths.
246
246
 
247
247
  When a task matches an advertised skill, read its `SKILL.md` before using other
248
248
  files in the skill directory.
@@ -910,20 +910,19 @@ select a global instruction file; its `skills` child is an additional Agent Skil
910
910
  | --- | --- |
911
911
  | `FORGERELAY_SKILLS` | Set to `0` to hide skills. Enabled by default. |
912
912
  | `FORGERELAY_SUBAGENTS` | Set to `1` to expose configured subagent profiles. |
913
- | `FORGERELAY_AGENT_DIR` | Defaults to `~/.codex`; its `skills` child is included as an additional Skill source. |
913
+ | `FORGERELAY_AGENT_DIR` | Defaults to `~/.codex`; used by supported Agent integrations, not as an automatic Skill source. |
914
914
  | `FORGERELAY_SKILL_PATHS` | Optional comma-separated additional skill directories. |
915
915
 
916
- Standard Agent Skills are discovered in precedence order from:
916
+ Skills are discovered in precedence order from:
917
917
 
918
918
  - project `.agents/skills`
919
- - `~/.agents/skills`
920
- - the active ForgeRelay config directory's `skills` folder
921
- - `FORGERELAY_AGENT_DIR/skills`
922
- - paths from `FORGERELAY_SKILL_PATHS`
919
+ - project `.forgerelay/skills`
920
+ - the active ForgeRelay config directory's `skills` folder (`~/.forgerelay/skills` by default)
921
+ - paths explicitly added through `FORGERELAY_SKILL_PATHS`
923
922
 
924
- The ownership boundaries are different even though all of these are readable Skill sources. Project/global `.agents/skills` belong to the open Agent Skills ecosystem and may contain files or symlinks installed by other Agent tooling. ForgeRelay-owned Skills stay under the active ForgeRelay config directory (`~/.forgerelay/skills` by default); ForgeRelay does not migrate or install its private Skills into `~/.agents/skills`.
923
+ Project `.agents/skills` belongs to the open Agent Skills ecosystem and may contain files or symlinks installed by other Agent tooling. ForgeRelay-owned project/system Skills stay under `.forgerelay/skills` and the active ForgeRelay config directory. Global Agent runtime directories such as `~/.agents/skills` and `FORGERELAY_AGENT_DIR/skills` are not scanned automatically.
925
924
 
926
- When the same Skill name appears in more than one source, the first source wins. Project Skills therefore override same-named global `~/.agents/skills` entries, matching ForgeRelay's project-over-global configuration model.
925
+ When the same Skill name appears in more than one source, the first source wins: project Agent Skills override project ForgeRelay Skills, which override system ForgeRelay Skills and explicit additional paths.
927
926
 
928
927
  When subagents are enabled, canonical v1.2 profiles are discovered from:
929
928
 
package/docs/gotchas.md CHANGED
@@ -192,15 +192,14 @@ Skills are enabled by default. Check:
192
192
  FORGERELAY_SKILLS=1 forgerelay serve
193
193
  ```
194
194
 
195
- Standard paths include:
195
+ Standard automatic discovery paths include:
196
196
 
197
197
  - project `.agents/skills`
198
- - `~/.agents/skills`
198
+ - project `.forgerelay/skills`
199
199
  - active ForgeRelay config `skills` directory (`~/.forgerelay/skills` by default)
200
- - `FORGERELAY_AGENT_DIR/skills`
201
- - additional `FORGERELAY_SKILL_PATHS`
200
+ - additional paths explicitly configured through `FORGERELAY_SKILL_PATHS`
202
201
 
203
- `.agents/skills` is an external/open Agent Skill source and may contain symlinks managed by other Agent tooling. ForgeRelay-owned Skills belong under its own config `skills` directory and must not be migrated into `.agents/skills`.
202
+ ForgeRelay does **not** automatically scan global Agent runtime Skill directories such as `~/.agents/skills` or `FORGERELAY_AGENT_DIR/skills`. Project `.agents/skills` remains the open Agent Skills source, while ForgeRelay-owned project/system Skills live under `.forgerelay/skills` and the active ForgeRelay config `skills` directory.
204
203
 
205
204
  ## Subagent profiles do not appear
206
205
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akira-tl/forgerelay",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
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",
@@ -67,7 +67,7 @@
67
67
  "release:push-ready": "node scripts/release/push-ready.mjs",
68
68
  "postinstall": "node scripts/fix-node-pty-permissions.mjs",
69
69
  "start": "node dist/cli.js serve",
70
- "test": "node --test scripts/debug/runtime.test.mjs scripts/config/schema-text.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/definition/schema.test.ts && tsx src/runtime/config/resolution/resolver.test.ts && tsx src/runtime/security/project-execution-trust.test.ts && tsx src/runtime/config/resolution/hooks.test.ts && tsx src/workspaces/state/project-context.test.ts && tsx src/runtime/config/config.test.ts && tsx src/runtime/config/external-mcp-registry.test.ts && tsx src/runtime/config/external-mcp-auth-store.test.ts && tsx src/mcp/operations/external-mcp/external-mcp-oauth.test.ts && tsx src/cli/mcp/status.test.ts && tsx src/cli/mcp/diagnostics.test.ts && tsx src/cli/mcp/external-mcp.test.ts && tsx src/runtime/shell/command-shell-runtime.test.ts && tsx src/runtime/instructions/shell-instructions.test.ts && tsx src/runtime/instructions/powershell-skill.test.ts && tsx src/cli/shell/setup.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/operations/project-trust.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/hooks/hooks-trust.test.ts && tsx src/mcp/operations/external-mcp/external-mcp-transform.test.ts && tsx src/mcp/hooks/external-mcp-transform-trust.test.ts && tsx src/mcp/operations/external-mcp/external-mcp-trust.test.ts && tsx src/mcp/server/core/capability-registry.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/mcp/request-context.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/mcp/server/operations/hooks.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/project-trust.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",
70
+ "test": "node --test scripts/debug/runtime.test.mjs scripts/config/schema-text.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/workspaces/relay/tests/composite.test.ts && tsx src/runtime/config/definition/schema.test.ts && tsx src/runtime/config/resolution/resolver.test.ts && tsx src/runtime/security/project-execution-trust.test.ts && tsx src/runtime/config/resolution/hooks.test.ts && tsx src/workspaces/state/project-context.test.ts && tsx src/runtime/config/config.test.ts && tsx src/runtime/config/external-mcp-registry.test.ts && tsx src/runtime/config/external-mcp-auth-store.test.ts && tsx src/mcp/operations/external-mcp/external-mcp-oauth.test.ts && tsx src/cli/mcp/status.test.ts && tsx src/cli/mcp/diagnostics.test.ts && tsx src/cli/mcp/external-mcp.test.ts && tsx src/runtime/shell/command-shell-runtime.test.ts && tsx src/runtime/instructions/shell-instructions.test.ts && tsx src/runtime/instructions/powershell-skill.test.ts && tsx src/cli/shell/setup.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/operations/project-trust.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/hooks/hooks-trust.test.ts && tsx src/mcp/operations/external-mcp/external-mcp-transform.test.ts && tsx src/mcp/hooks/external-mcp-transform-trust.test.ts && tsx src/mcp/operations/external-mcp/external-mcp-trust.test.ts && tsx src/mcp/server/core/capability-registry.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/mcp/request-context.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/mcp/server/operations/hooks.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/project-trust.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",
71
71
  "typecheck": "tsc -p tsconfig.json --noEmit",
72
72
  "release:check": "node scripts/release-version.mjs check",
73
73
  "release:tag-check": "node scripts/release-version.mjs tag",
@@ -13,6 +13,7 @@ import {
13
13
  } from "node:fs";
14
14
  import { join, resolve } from "node:path";
15
15
  import { setTimeout as delay } from "node:timers/promises";
16
+ import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
16
17
  import { debugRoot, repoRoot } from "../runtime.mjs";
17
18
 
18
19
  export function relayAcceptanceTopology() {
@@ -170,6 +171,37 @@ export function toolText(result) {
170
171
  .join("\n");
171
172
  }
172
173
 
174
+ export async function assertStatelessRelayPanel({ mcpUrl, accessToken, executionCheckout, expectedWorkspaceId }) {
175
+ const transport = new StreamableHTTPClientTransport(new URL(mcpUrl), {
176
+ requestInit: { headers: { authorization: `Bearer ${accessToken}` } },
177
+ });
178
+ const client = new Client(
179
+ { name: "forgerelay-relay-stateless-acceptance", version: "1.0.0" },
180
+ { versionNegotiation: { mode: { pin: "2026-07-28" } } },
181
+ );
182
+ try {
183
+ await client.connect(transport);
184
+ assert.equal(transport.sessionId, undefined);
185
+ const opened = await client.callTool({
186
+ name: "open_workspace",
187
+ arguments: { path: executionCheckout, relay: "execution", context: "none" },
188
+ });
189
+ assertToolOk(opened, "open relayed checkout over stateless HTTP");
190
+ assert.equal(opened.structuredContent.workspaceId, expectedWorkspaceId);
191
+ const panel = await client.callTool({
192
+ name: "activity_panel",
193
+ arguments: { workspaceId: expectedWorkspaceId },
194
+ _meta: { "dev.forgerelay/conversation": "relay-stateless-panel-acceptance" },
195
+ });
196
+ assertToolOk(panel, "render relayed Activity Panel over stateless HTTP");
197
+ assert.match(String(panel.structuredContent.turnId), /^turn_/);
198
+ assert.equal(panel._meta?.["forgerelay/activityPanelWorkspace"]?.workspaceId, expectedWorkspaceId);
199
+ } finally {
200
+ await client.close().catch(() => undefined);
201
+ }
202
+ pass("relay stateless Panel", "relayed open_workspace and activity_panel succeeded across modern stateless MCP requests");
203
+ }
204
+
173
205
  export function pass(label, detail) {
174
206
  console.log(`PASS ${label}: ${detail}`);
175
207
  }
@@ -2,23 +2,19 @@ import assert from "node:assert/strict";
2
2
  import { spawn, spawnSync } from "node:child_process";
3
3
  import { createHash, randomBytes, randomUUID } from "node:crypto";
4
4
  import { once } from "node:events";
5
- import { connect } from "node:net";
6
- import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
7
- import { join, resolve } from "node:path";
5
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
+ import { join } from "node:path";
8
7
  import { setTimeout as delay } from "node:timers/promises";
9
- import { debugRoot, repoRoot } from "./runtime.mjs";
10
- import { relayAcceptanceTopology, assertPortsFree, setupGitProject, runGit, taskStateFiles, findTaskStateFile, assertTaskBodyAbsentFromGateway, assertSafeRelayInspection, installRelayImageFixture, assertRelayedImageResult, assertToolOk, toolText, pass } from "./relay-accept/support.mjs";
11
-
8
+ import { repoRoot } from "./runtime.mjs";
9
+ import { relayAcceptanceTopology, assertPortsFree, setupGitProject, findTaskStateFile, assertTaskBodyAbsentFromGateway, assertSafeRelayInspection, installRelayImageFixture, assertRelayedImageResult, assertStatelessRelayPanel, assertToolOk, toolText, pass } from "./relay-accept/support.mjs";
12
10
  const {
13
- gatewayPort, executionPort, gatewayBaseUrl, gatewayMcpUrl, executionBaseUrl,
14
- acceptanceRoot, gatewayConfigDir, gatewayStateDir, gatewayWorktreeRoot,
15
- gatewayProjects, gatewayLocalProject, executionConfigDir, executionStateDir,
16
- executionWorktreeRoot, executionProjects, executionCheckout, executionWorktreeSource,
17
- bootstrapSecret, checkoutTaskBody, worktreeTaskBody, compositeMemberTaskBody, compositeTaskBody,
11
+ gatewayPort, executionPort, gatewayBaseUrl, gatewayMcpUrl, executionBaseUrl, acceptanceRoot,
12
+ gatewayConfigDir, gatewayStateDir, gatewayWorktreeRoot, gatewayProjects, gatewayLocalProject,
13
+ executionConfigDir, executionStateDir, executionWorktreeRoot, executionProjects, executionCheckout,
14
+ executionWorktreeSource, bootstrapSecret, checkoutTaskBody, worktreeTaskBody, compositeMemberTaskBody, compositeTaskBody,
18
15
  } = relayAcceptanceTopology();
19
16
  const gatewayOwnerToken = randomBytes(32).toString("base64url");
20
17
  const executionOwnerToken = randomBytes(32).toString("base64url");
21
-
22
18
  await assertPortsFree([gatewayPort, executionPort]);
23
19
  rmSync(acceptanceRoot, { recursive: true, force: true });
24
20
  mkdirSync(gatewayLocalProject, { recursive: true });
@@ -30,10 +26,8 @@ writeFileSync(join(gatewayLocalProject, "sentinel.txt"), "gateway-local-content\
30
26
  writeFileSync(join(executionCheckout, "sentinel.txt"), "execution-remote-content\n");
31
27
  writeFileSync(join(executionCheckout, "AGENTS.md"), `${bootstrapSecret}\n`);
32
28
  setupGitProject(executionWorktreeSource);
33
-
34
29
  writeAuthFile(gatewayConfigDir, gatewayOwnerToken, "relay-acceptance-gateway");
35
30
  writeAuthFile(executionConfigDir, executionOwnerToken, "relay-acceptance-execution");
36
-
37
31
  const executionEnv = instanceEnv({
38
32
  port: executionPort,
39
33
  baseUrl: executionBaseUrl,
@@ -106,6 +100,13 @@ try {
106
100
  assert.equal(openedB.structuredContent.workspaceId, checkoutRelayId);
107
101
  pass("relay persistent identity", `two Gateway conversations reused ${checkoutRelayId}`);
108
102
 
103
+ await assertStatelessRelayPanel({
104
+ mcpUrl: gatewayMcpUrl,
105
+ accessToken: oauth.accessToken,
106
+ executionCheckout,
107
+ expectedWorkspaceId: checkoutRelayId,
108
+ });
109
+
109
110
  assertRelayedImageResult(callTool(gatewayMcpUrl, oauth.accessToken, sessionB, nextId(), "read", {
110
111
  workspaceId: checkoutRelayId, path: "relay-image.png",
111
112
  }, conversationB), relayImageBase64, gatewayStateDir);