@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 +19 -0
- package/dist/mcp/oauth/router.js +1 -1
- package/dist/mcp/panel/app.js +3 -2
- package/dist/mcp/server/transport/http-server.js +7 -2
- package/dist/server.js +21 -3
- package/dist/workspaces/presentation/workspace-presentation.js +21 -0
- package/dist/workspaces/relay/tests/test-support.js +1 -0
- package/dist/workspaces/resources/skills.js +1 -2
- package/docs/chatgpt-coding-workflow.md +8 -8
- package/docs/configuration.md +7 -8
- package/docs/gotchas.md +4 -5
- package/package.json +2 -2
- package/scripts/debug/relay-accept/support.mjs +32 -0
- package/scripts/debug/relay-accept.mjs +15 -14
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
|
package/dist/mcp/oauth/router.js
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 }));
|
package/dist/mcp/panel/app.js
CHANGED
|
@@ -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:
|
|
69
|
-
connectDomains:
|
|
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 (
|
|
446
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
236
|
+
ForgeRelay discovers Skills in precedence order from:
|
|
236
237
|
|
|
237
238
|
- project `.agents/skills`
|
|
238
|
-
-
|
|
239
|
-
- the active ForgeRelay config directory's `skills` folder
|
|
240
|
-
-
|
|
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
|
|
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
|
|
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.
|
package/docs/configuration.md
CHANGED
|
@@ -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`;
|
|
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
|
-
|
|
916
|
+
Skills are discovered in precedence order from:
|
|
917
917
|
|
|
918
918
|
- project `.agents/skills`
|
|
919
|
-
-
|
|
920
|
-
- the active ForgeRelay config directory's `skills` folder
|
|
921
|
-
- `
|
|
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
|
-
|
|
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
|
|
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
|
-
-
|
|
198
|
+
- project `.forgerelay/skills`
|
|
199
199
|
- active ForgeRelay config `skills` directory (`~/.forgerelay/skills` by default)
|
|
200
|
-
- `
|
|
201
|
-
- additional `FORGERELAY_SKILL_PATHS`
|
|
200
|
+
- additional paths explicitly configured through `FORGERELAY_SKILL_PATHS`
|
|
202
201
|
|
|
203
|
-
|
|
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.
|
|
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 {
|
|
6
|
-
import {
|
|
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 {
|
|
10
|
-
import { relayAcceptanceTopology, assertPortsFree, setupGitProject,
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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);
|