@akira-tl/forgerelay 0.2.6 → 0.3.0
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 +18 -0
- package/README.md +15 -0
- package/capabilities/artifacts-review/GUIDE.md +42 -0
- package/capabilities/host-integration/GUIDE.md +68 -0
- package/capabilities/lifecycle-hooks/GUIDE.md +39 -0
- package/capabilities/managed-worktrees/GUIDE.md +43 -0
- package/capabilities/shell-processes/GUIDE.md +51 -0
- package/capabilities/subagents/GUIDE.md +69 -0
- package/dist/advertised-files.js +23 -0
- package/dist/capabilities.js +99 -0
- package/dist/cli.js +1 -3
- package/dist/config.js +11 -3
- package/dist/logger.js +1 -12
- package/dist/mcp/server-instructions.js +15 -30
- package/dist/server.js +42 -13
- package/dist/skills.js +11 -30
- package/dist/workspaces.js +16 -0
- package/docs/chatgpt-coding-workflow.md +55 -13
- package/docs/configuration.md +45 -2
- package/docs/roadmap.md +29 -3
- package/docs/security.md +24 -7
- package/package.json +4 -3
- package/scripts/debug/accept.mjs +38 -3
- package/scripts/ensure-cli-executable.mjs +12 -0
|
@@ -14,57 +14,42 @@ export const toolNames = {
|
|
|
14
14
|
writeStdin: "write_stdin",
|
|
15
15
|
};
|
|
16
16
|
export function buildShellMutationPolicy() {
|
|
17
|
-
return "Shell commands may modify ordinary project files when that is a natural part of the user's requested development task. Never use shell commands to modify security- or privilege-sensitive operating-system files or credential material such as /etc/sudoers, /etc/passwd, /etc/shadow, PAM or authentication policy, SSH private keys, or equivalent privileged system files. Modify configuration files through shell only when the user's request explicitly calls for that configuration change; do not infer permission merely because changing configuration would be convenient.";
|
|
17
|
+
return "Shell commands may modify ordinary project files when that is a natural part of the user's requested development task. They may also perform external device or hardware mutations when the user's current request explicitly asks for the actual device-changing operation, including firmware flashing or equivalent persistent device updates; do not infer such authorization from a check, audit, probe, backup, verification, dry-run, or build-only request. Never use shell commands to modify security- or privilege-sensitive operating-system files or credential material such as /etc/sudoers, /etc/passwd, /etc/shadow, PAM or authentication policy, SSH private keys, or equivalent privileged system files. Modify configuration files through shell only when the user's request explicitly calls for that configuration change; do not infer permission merely because changing configuration would be convenient.";
|
|
18
18
|
}
|
|
19
|
-
export function buildServerInstructions(config
|
|
20
|
-
return joinInstructions(capabilityContractInstructions(config
|
|
19
|
+
export function buildServerInstructions(config) {
|
|
20
|
+
return joinInstructions(capabilityContractInstructions(config), selectedWorkflowInstructions(config), config.appendInstructions);
|
|
21
21
|
}
|
|
22
22
|
export function buildToolDescriptions(config) {
|
|
23
23
|
const skillCapability = config.skillsEnabled
|
|
24
|
-
? " Advertised skill paths may be outside the workspace
|
|
24
|
+
? " Advertised skill paths may also be outside the workspace."
|
|
25
25
|
: "";
|
|
26
26
|
const shellSurface = config.toolMode === "minimal"
|
|
27
27
|
? ` In minimal tool mode, ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} are disabled, so shell commands may be used for equivalent search and directory inspection.`
|
|
28
28
|
: "";
|
|
29
|
-
const shellMutationPolicy = buildShellMutationPolicy();
|
|
30
29
|
return {
|
|
31
|
-
read: `Read a file inside an open workspace or the OS temp directory. Instruction files returned by ${toolNames.openWorkspace}
|
|
30
|
+
read: `Read a file inside an open workspace or the OS temp directory. Instruction files and advertised capability guides returned by ${toolNames.openWorkspace} are also readable when applicable.${skillCapability} Only advertised entry files and files under already-loaded advertised directories are readable outside the normal roots. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
32
31
|
write: `Create or completely overwrite a file inside an open workspace or the OS temp directory. Workspace paths may be relative; OS temp paths may be absolute. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
33
32
|
edit: `Edit one file inside an open workspace or the OS temp directory by replacing exact text blocks. Each oldText must match a unique, non-overlapping region of the original file. Workspace paths may be relative; OS temp paths may be absolute. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
34
33
|
rename: `Rename or move one file or directory inside an open workspace or the OS temp directory without overwriting an existing destination. Source and destination must both remain inside the permitted file roots. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
35
34
|
delete: `Delete one file or directory inside an open workspace or the OS temp directory. Non-empty directories require recursive=true. An allowed root itself cannot be deleted. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
36
35
|
applyPatch: `Apply one Codex-style patch inside an open workspace or the OS temp directory. Supports adding, overwriting, updating, deleting, and moving files. Workspace paths must remain relative; absolute paths are accepted only inside the OS temp directory. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
37
|
-
shell: `Run a shell command inside an open workspace.${shellSurface} Commands execute with the local user's authority; workspace filesystem containment does not make shell execution a sandbox. ForgeRelay waits up to 300 seconds
|
|
36
|
+
shell: `Run a shell command inside an open workspace.${shellSurface} Commands execute with the local user's authority; workspace filesystem containment does not make shell execution a sandbox. ForgeRelay waits up to 300 seconds, then returns a processId for a still-running command; use ${toolNames.writeStdin} to poll, interact, wait, or send Ctrl-C. Completed background commands may be reported later for the same workspaceId. Call ${toolNames.openWorkspace} first and pass workspaceId. Expose this capability only behind strong authentication.`,
|
|
38
37
|
shellCommand: "Shell command to run with the local user's authority.",
|
|
39
38
|
};
|
|
40
39
|
}
|
|
41
|
-
function capabilityContractInstructions(config
|
|
42
|
-
const
|
|
43
|
-
?
|
|
44
|
-
: `
|
|
45
|
-
const
|
|
40
|
+
function capabilityContractInstructions(config) {
|
|
41
|
+
const staleWorkspacePolicy = config.toolMode === "codex"
|
|
42
|
+
? ""
|
|
43
|
+
: ` If ${toolNames.openWorkspace} reports logical workspaces idle for more than two days, let the user choose whether to resume or close them with ${toolNames.closeWorkspace}; never close them automatically.`;
|
|
44
|
+
const workspaceLifecycle = `Use ForgeRelay as a local coding workspace. Default to the user's existing checkout. Reuse the workspaceId returned by ${toolNames.openWorkspace} for this conversation; resume another logical workspaceId only when the user wants that workspace, and request a new logical workspace only when explicitly asked.${staleWorkspacePolicy} Only open mode=\"worktree\" when the user explicitly asks for isolated or parallel Git work. ${toolNames.closeWorkspace} releases a logical workspace; ${toolNames.closeWorktree} finalizes a managed worktree. Read the managed-worktrees capability guide for advanced worktree lifecycle and failure semantics.`;
|
|
45
|
+
const agents = `Follow instructions returned by ${toolNames.openWorkspace}. Read an availableAgentsFiles path before working under it.`;
|
|
46
|
+
const capabilityGuides = `When ${toolNames.openWorkspace} returns capability guides, use ${toolNames.read} to load only a task-relevant guide; do not preload all guides.`;
|
|
46
47
|
const skills = config.skillsEnabled
|
|
47
|
-
? `When
|
|
48
|
+
? `When a task matches an available skill from ${toolNames.openWorkspace}, read its advertised path before proceeding. Outside normal file roots, ${toolNames.read} permits only advertised entry files and files under already-loaded advertised directories.`
|
|
48
49
|
: "";
|
|
49
|
-
const toolSurface = toolSurfaceInstructions(config);
|
|
50
50
|
const shellMutationPolicy = buildShellMutationPolicy();
|
|
51
51
|
const hooks = "When a ForgeRelay tool result reports Hook results, tell the user which meaningful hooks ran and whether they passed or blocked the operation. Do not claim the requested operation succeeded when a blocking hook prevented it.";
|
|
52
|
-
|
|
53
|
-
? "When the user supplies or generates a file that is not present on the ForgeRelay host, use download_artifact with its native file value, the existing workspace ID, and a suitable relative destination path chosen from the user's request and project structure. The tool refuses to overwrite an existing destination and returns the normalized workspace-relative path. Use normal workspace tools when explicit inspection, replacement, movement, renaming, or deletion is needed. Do not recreate binary files with write/edit calls or place signed URLs, native file objects, base64 content, or invented host paths in shell commands or logs."
|
|
54
|
-
: "";
|
|
55
|
-
const showChanges = config.widgets === "changes"
|
|
56
|
-
? "If the turn successfully modifies files by creating, editing, overwriting, deleting, moving, or applying patches, call show_changes exactly once for that workspace after the final related file change and before your final response so the user can inspect the aggregate diff for that turn. Do not call it after every individual file change; do not skip it because individual file-change tools already returned diffs."
|
|
57
|
-
: "";
|
|
58
|
-
return joinInstructions(workspaceLifecycle, agents, skills, toolSurface, shellMutationPolicy, hooks, artifact, showChanges);
|
|
59
|
-
}
|
|
60
|
-
function toolSurfaceInstructions(config) {
|
|
61
|
-
if (config.toolMode === "codex") {
|
|
62
|
-
return `In codex tool mode, workspace file and command operations use ${toolNames.read}, ${toolNames.rename}, ${toolNames.delete}, apply_patch, exec_command, and ${toolNames.writeStdin}.`;
|
|
63
|
-
}
|
|
64
|
-
if (config.toolMode === "full") {
|
|
65
|
-
return `In full tool mode, dedicated ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} inspection tools are available alongside the core workspace tools. ${toolNames.writeStdin} is available for running bash processes.`;
|
|
66
|
-
}
|
|
67
|
-
return `In minimal tool mode, dedicated ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} inspection tools are disabled; the core workspace tools remain available, including ${toolNames.writeStdin} for running bash processes.`;
|
|
52
|
+
return joinInstructions(workspaceLifecycle, agents, capabilityGuides, skills, shellMutationPolicy, hooks);
|
|
68
53
|
}
|
|
69
54
|
function selectedWorkflowInstructions(config) {
|
|
70
55
|
if (config.workflowInstructions === false)
|
package/dist/server.js
CHANGED
|
@@ -14,6 +14,7 @@ import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE, } from "@mode
|
|
|
14
14
|
import express from "express";
|
|
15
15
|
import * as z from "zod/v4";
|
|
16
16
|
import { applyPatch } from "./apply-patch.js";
|
|
17
|
+
import { buildCapabilityFingerprint } from "./capabilities.js";
|
|
17
18
|
import { deletePath, renamePath } from "./file-mutations.js";
|
|
18
19
|
import { isArtifactDownloadSupportedPlatform, registerArtifactTools, } from "./artifact-tools.js";
|
|
19
20
|
import { loadConfig } from "./config.js";
|
|
@@ -112,6 +113,17 @@ const workspaceSkillOutputSchema = z.object({
|
|
|
112
113
|
description: z.string(),
|
|
113
114
|
path: z.string(),
|
|
114
115
|
});
|
|
116
|
+
const capabilityFingerprintOutputSchema = z.object({
|
|
117
|
+
version: z.string(),
|
|
118
|
+
toolMode: z.enum(["minimal", "full", "codex"]),
|
|
119
|
+
capabilities: z.array(z.string()),
|
|
120
|
+
});
|
|
121
|
+
const capabilityGuideOutputSchema = z.object({
|
|
122
|
+
name: z.string(),
|
|
123
|
+
description: z.string(),
|
|
124
|
+
whenToRead: z.string(),
|
|
125
|
+
path: z.string(),
|
|
126
|
+
});
|
|
115
127
|
const workspaceAgentsFileOutputSchema = z.object({
|
|
116
128
|
path: z.string(),
|
|
117
129
|
content: z.string(),
|
|
@@ -154,7 +166,7 @@ function sendJsonRpcError(res, status, code, message) {
|
|
|
154
166
|
}
|
|
155
167
|
function requestLogFields(req, config) {
|
|
156
168
|
return {
|
|
157
|
-
ip: requestIp(req
|
|
169
|
+
ip: requestIp(req),
|
|
158
170
|
host: req.header("host"),
|
|
159
171
|
userAgent: req.header("user-agent"),
|
|
160
172
|
origin: req.header("origin"),
|
|
@@ -639,9 +651,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
639
651
|
version: FORGERELAY_VERSION,
|
|
640
652
|
description: "Secure local coding workspace for MCP clients. Provides workspace-scoped file, search, edit, write, and shell tools.",
|
|
641
653
|
}, {
|
|
642
|
-
instructions: buildServerInstructions(config,
|
|
643
|
-
artifactDownloadSupported: isArtifactDownloadSupportedPlatform(),
|
|
644
|
-
}),
|
|
654
|
+
instructions: buildServerInstructions(config),
|
|
645
655
|
});
|
|
646
656
|
const currentWorkspaceAppUri = currentWorkspaceAppIdentity().uri;
|
|
647
657
|
const workspaceAppResourceMetadata = {
|
|
@@ -660,7 +670,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
660
670
|
}, async (uri, _variables, extra) => readWorkspaceAppResource(config, uri.toString(), extra.sessionId));
|
|
661
671
|
registerAppTool(server, "open_workspace", {
|
|
662
672
|
title: "Open workspace",
|
|
663
|
-
description: "Open or resume a local coding workspace.
|
|
673
|
+
description: "Open or resume a local coding workspace. Reuse the returned workspaceId for later calls. Default to checkout; use mode=\"worktree\" only when the user explicitly requests isolated or parallel Git work. Every call returns a capability fingerprint; bootstrap calls also expose project context, skills, and capability guides when needed.",
|
|
664
674
|
inputSchema: {
|
|
665
675
|
path: z
|
|
666
676
|
.string()
|
|
@@ -724,6 +734,8 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
724
734
|
targetBranch: z.string().optional(),
|
|
725
735
|
managed: z.boolean(),
|
|
726
736
|
})),
|
|
737
|
+
capabilityFingerprint: capabilityFingerprintOutputSchema,
|
|
738
|
+
capabilityGuides: z.array(capabilityGuideOutputSchema).optional(),
|
|
727
739
|
agentsFiles: z.array(workspaceAgentsFileOutputSchema).optional(),
|
|
728
740
|
availableAgentsFiles: z.array(workspaceAvailableAgentsFileOutputSchema).optional(),
|
|
729
741
|
skills: z.array(workspaceSkillOutputSchema).optional(),
|
|
@@ -747,6 +759,9 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
747
759
|
});
|
|
748
760
|
const knownWorktrees = await workspaces.listKnownWorktrees(workspace);
|
|
749
761
|
const staleWorkspaces = await workspaces.listStaleWorkspaces(workspace);
|
|
762
|
+
const capabilityFingerprint = buildCapabilityFingerprint(config, FORGERELAY_VERSION, {
|
|
763
|
+
artifactDownloadSupported: isArtifactDownloadSupportedPlatform(),
|
|
764
|
+
});
|
|
750
765
|
if (config.widgets === "changes") {
|
|
751
766
|
await reviewCheckpoints.initializeWorkspace({
|
|
752
767
|
workspaceId: workspace.id,
|
|
@@ -760,6 +775,12 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
760
775
|
description: skill.description,
|
|
761
776
|
path: formatPathForPrompt(skill.filePath),
|
|
762
777
|
}));
|
|
778
|
+
const capabilityGuides = workspace.capabilityGuides.map((guide) => ({
|
|
779
|
+
name: guide.name,
|
|
780
|
+
description: guide.description,
|
|
781
|
+
whenToRead: guide.whenToRead,
|
|
782
|
+
path: formatPathForPrompt(guide.filePath),
|
|
783
|
+
}));
|
|
763
784
|
const cardAgentProviders = config.subagents ? localAgentProviders : [];
|
|
764
785
|
const cardAgents = workspace.agentProfiles.map((profile) => {
|
|
765
786
|
const summary = summarizeLocalAgentProfile(profile);
|
|
@@ -778,13 +799,14 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
778
799
|
path: formatAgentsPath(file.path, workspace.root),
|
|
779
800
|
}));
|
|
780
801
|
const visibleSkills = includeBootstrapContext ? cardSkills : [];
|
|
802
|
+
const visibleCapabilityGuides = includeBootstrapContext ? capabilityGuides : [];
|
|
781
803
|
const visibleAgentProviders = includeBootstrapContext ? cardAgentProviders : [];
|
|
782
804
|
const visibleAgents = includeBootstrapContext ? cardAgents : [];
|
|
783
805
|
const loadedAgentsFiles = includeBootstrapContext ? cardAgentsFiles : [];
|
|
784
806
|
const availableAgentsFileOutputs = includeBootstrapContext ? cardAvailableAgentsFiles : [];
|
|
785
807
|
const cardInstruction = config.skillsEnabled
|
|
786
|
-
? "Use this workspaceId in all subsequent tool calls for this project.
|
|
787
|
-
: "Use this workspaceId in all subsequent tool calls for this project.
|
|
808
|
+
? "Use this workspaceId in all subsequent tool calls for this project. Follow loaded agentsFiles instructions. Read an availableAgentsFiles path before working under it. When a task matches an available skill or capability guide, read its advertised path before proceeding."
|
|
809
|
+
: "Use this workspaceId in all subsequent tool calls for this project. Follow loaded agentsFiles instructions. Read an availableAgentsFiles path before working under it. When a task matches a capability guide, read its advertised path before proceeding.";
|
|
788
810
|
const instruction = workspaceReused
|
|
789
811
|
? includeBootstrapContext
|
|
790
812
|
? [
|
|
@@ -795,7 +817,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
795
817
|
: [
|
|
796
818
|
`Workspace already open as ${workspace.id}.`,
|
|
797
819
|
"Reuse this workspaceId for subsequent tool calls. This is the same directory previously opened in this conversation.",
|
|
798
|
-
"Continue following the project instructions, nested instruction files, skills, agent profiles, and diagnostics previously provided for this workspace. They remain active and are not repeated here.",
|
|
820
|
+
"Continue following the project instructions, nested instruction files, skills, capability guides, agent profiles, and diagnostics previously provided for this workspace. They remain active and are not repeated here.",
|
|
799
821
|
].join("\n\n")
|
|
800
822
|
: workspace.mode === "worktree"
|
|
801
823
|
? "Use this workspaceId for subsequent tool calls. Follow the project instructions, nested instruction files, skills, agent profiles, and diagnostics returned for this isolated worktree."
|
|
@@ -820,6 +842,9 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
820
842
|
visibleSkills.length > 0
|
|
821
843
|
? `Available skills: ${visibleSkills.map((skill) => skill.name).join(", ")}`
|
|
822
844
|
: undefined,
|
|
845
|
+
visibleCapabilityGuides.length > 0
|
|
846
|
+
? `Capability guides: ${visibleCapabilityGuides.map((guide) => guide.name).join(", ")}`
|
|
847
|
+
: undefined,
|
|
823
848
|
visibleAgentProviders.some((provider) => provider.available)
|
|
824
849
|
? `Available subagent providers: ${visibleAgentProviders.filter((provider) => provider.available).map((provider) => provider.name).join(", ")}`
|
|
825
850
|
: undefined,
|
|
@@ -835,6 +860,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
835
860
|
staleWorkspaces.length > 0
|
|
836
861
|
? `Idle logical workspaces for this same physical workspace (>2 days): ${staleWorkspaces.map((stale) => `${stale.workspaceId} last-used=${stale.lastUsedAt}`).join(", ")}. Tell the user these are available to resume or explicitly close; do not clean them up automatically.`
|
|
837
862
|
: undefined,
|
|
863
|
+
`ForgeRelay ${capabilityFingerprint.version} capabilities: ${capabilityFingerprint.capabilities.join(", ")}`,
|
|
838
864
|
instruction,
|
|
839
865
|
].filter(Boolean).join("\n"),
|
|
840
866
|
},
|
|
@@ -861,6 +887,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
861
887
|
worktree: workspace.worktree,
|
|
862
888
|
worktrees: knownWorktrees,
|
|
863
889
|
staleWorkspaces,
|
|
890
|
+
capabilityFingerprint,
|
|
864
891
|
agentsFiles: cardAgentsFiles,
|
|
865
892
|
availableAgentsFiles: cardAvailableAgentsFiles,
|
|
866
893
|
skills: cardSkills,
|
|
@@ -885,8 +912,10 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
885
912
|
worktree: workspace.worktree,
|
|
886
913
|
worktrees: knownWorktrees,
|
|
887
914
|
staleWorkspaces,
|
|
915
|
+
capabilityFingerprint,
|
|
888
916
|
...(includeBootstrapContext
|
|
889
917
|
? {
|
|
918
|
+
capabilityGuides: visibleCapabilityGuides,
|
|
890
919
|
agentsFiles: loadedAgentsFiles,
|
|
891
920
|
availableAgentsFiles: availableAgentsFileOutputs,
|
|
892
921
|
skills: visibleSkills,
|
|
@@ -901,7 +930,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
901
930
|
});
|
|
902
931
|
registerAppTool(server, toolNames.closeWorkspace, {
|
|
903
932
|
title: "Close logical workspace",
|
|
904
|
-
description: "Release one logical
|
|
933
|
+
description: "Release one logical workspaceId after the user chooses cleanup. This does not delete checkout files. Use close_worktree to finalize and remove a managed worktree. Running or unconsumed processes prevent closure.",
|
|
905
934
|
inputSchema: {
|
|
906
935
|
workspaceId: z.string().describe("Logical workspace ID to release."),
|
|
907
936
|
},
|
|
@@ -929,7 +958,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
929
958
|
});
|
|
930
959
|
registerAppTool(server, toolNames.closeWorktree, {
|
|
931
960
|
title: "Close worktree",
|
|
932
|
-
description: "
|
|
961
|
+
description: "Finalize a managed worktree after its task is complete and verified. ForgeRelay may commit remaining changes, integrate the target branch when safe, and clean up the managed worktree. Read the managed-worktrees capability guide for advanced close, safety, and failure semantics.",
|
|
933
962
|
inputSchema: {
|
|
934
963
|
workspaceId: z
|
|
935
964
|
.string()
|
|
@@ -1010,8 +1039,8 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1010
1039
|
path: z
|
|
1011
1040
|
.string()
|
|
1012
1041
|
.describe(config.skillsEnabled
|
|
1013
|
-
? "File path to read, relative to the workspace root or absolute inside the OS temp directory. May also be an advertised skill path from open_workspace
|
|
1014
|
-
: "File path to read, relative to the workspace root or absolute inside the OS temp directory."),
|
|
1042
|
+
? "File path to read, relative to the workspace root or absolute inside the OS temp directory. May also be an advertised skill or capability-guide path from open_workspace, including a ~/... home-relative path."
|
|
1043
|
+
: "File path to read, relative to the workspace root or absolute inside the OS temp directory. May also be an advertised capability-guide path from open_workspace."),
|
|
1015
1044
|
offset: z
|
|
1016
1045
|
.number()
|
|
1017
1046
|
.int()
|
|
@@ -1846,7 +1875,7 @@ export function createServer(config = loadConfig(), options = {}) {
|
|
|
1846
1875
|
}, MCP_TRANSPORT_CLEANUP_INTERVAL_MS);
|
|
1847
1876
|
transportCleanupTimer.unref();
|
|
1848
1877
|
if (config.logging.trustProxy) {
|
|
1849
|
-
app.set("trust proxy",
|
|
1878
|
+
app.set("trust proxy", 1);
|
|
1850
1879
|
}
|
|
1851
1880
|
app.use((req, res, next) => {
|
|
1852
1881
|
const requestId = randomUUID();
|
package/dist/skills.js
CHANGED
|
@@ -1,27 +1,16 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join, resolve, sep } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { markAdvertisedFileSourceActivated, resolveAdvertisedFileReadPath, } from "./advertised-files.js";
|
|
5
5
|
import { loadSkills, } from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import { expandHomePath
|
|
6
|
+
import { expandHomePath } from "./roots.js";
|
|
7
7
|
const SUBAGENT_DELEGATION_NAME = "subagent-delegation";
|
|
8
|
-
const SUBAGENT_DELEGATION_SKILL = join(SUBAGENT_DELEGATION_NAME, "SKILL.md");
|
|
9
|
-
function bundledSkillsDir() {
|
|
10
|
-
return fileURLToPath(new URL("../skills", import.meta.url));
|
|
11
|
-
}
|
|
12
|
-
function hasSubagentDelegationSkill(skillDir) {
|
|
13
|
-
return existsSync(join(skillDir, SUBAGENT_DELEGATION_SKILL));
|
|
14
|
-
}
|
|
15
8
|
export function effectiveSkillPaths(config, cwd) {
|
|
16
|
-
const bundledSkills = bundledSkillsDir();
|
|
17
9
|
const defaultPathCandidates = [
|
|
18
10
|
join(homedir(), ".agents", "skills"),
|
|
19
11
|
resolve(cwd, ".agents", "skills"),
|
|
20
12
|
config.devspaceSkillsDir,
|
|
21
13
|
join(config.agentDir, "skills"),
|
|
22
|
-
config.subagents && !hasSubagentDelegationSkill(config.devspaceSkillsDir)
|
|
23
|
-
? bundledSkills
|
|
24
|
-
: undefined,
|
|
25
14
|
];
|
|
26
15
|
const defaultPaths = defaultPathCandidates.filter((path) => path !== undefined && existsSync(path));
|
|
27
16
|
const seen = new Set();
|
|
@@ -57,25 +46,17 @@ export function loadWorkspaceSkills(config, cwd) {
|
|
|
57
46
|
};
|
|
58
47
|
}
|
|
59
48
|
export function resolveSkillReadPath(skills, activatedSkillDirs, inputPath) {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const baseDir = resolve(skill.baseDir);
|
|
69
|
-
if (!activatedSkillDirs.has(baseDir))
|
|
70
|
-
continue;
|
|
71
|
-
if (!isPathInsideRoot(absolutePath, baseDir))
|
|
72
|
-
continue;
|
|
73
|
-
return { absolutePath, skill, isSkillFile: false };
|
|
74
|
-
}
|
|
75
|
-
return undefined;
|
|
49
|
+
const resolution = resolveAdvertisedFileReadPath(skills, activatedSkillDirs, inputPath);
|
|
50
|
+
if (!resolution)
|
|
51
|
+
return undefined;
|
|
52
|
+
return {
|
|
53
|
+
absolutePath: resolution.absolutePath,
|
|
54
|
+
skill: resolution.source,
|
|
55
|
+
isSkillFile: resolution.isEntryFile,
|
|
56
|
+
};
|
|
76
57
|
}
|
|
77
58
|
export function markSkillActivated(activatedSkillDirs, skill) {
|
|
78
|
-
activatedSkillDirs
|
|
59
|
+
markAdvertisedFileSourceActivated(activatedSkillDirs, skill);
|
|
79
60
|
}
|
|
80
61
|
export function formatPathForPrompt(path) {
|
|
81
62
|
const home = resolve(homedir());
|
package/dist/workspaces.js
CHANGED
|
@@ -2,6 +2,7 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import { mkdir, opendir, readFile, realpath, stat } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { loadCapabilityGuides, markCapabilityGuideActivated, resolveCapabilityGuideReadPath, } from "./capabilities.js";
|
|
5
6
|
import { HookRunner } from "./hooks.js";
|
|
6
7
|
import { closeManagedWorktree, createManagedWorktree, resolveManagedWorktreeBase, } from "./git-worktrees.js";
|
|
7
8
|
import { AccessDeniedError, assertAllowedPath, isPathInsideRoot, resolveAllowedPath, } from "./roots.js";
|
|
@@ -548,8 +549,10 @@ export class WorkspaceRegistry {
|
|
|
548
549
|
}
|
|
549
550
|
: undefined,
|
|
550
551
|
...this.loadSkillsForWorkspace(root),
|
|
552
|
+
capabilityGuides: loadCapabilityGuides(this.config),
|
|
551
553
|
agentProfiles: [],
|
|
552
554
|
activatedSkillDirs: new Set(),
|
|
555
|
+
activatedCapabilityGuideDirs: new Set(),
|
|
553
556
|
};
|
|
554
557
|
if (touch)
|
|
555
558
|
this.store?.touchSession(session.id);
|
|
@@ -582,6 +585,14 @@ export class WorkspaceRegistry {
|
|
|
582
585
|
skillRead,
|
|
583
586
|
};
|
|
584
587
|
}
|
|
588
|
+
const capabilityGuideRead = resolveCapabilityGuideReadPath(workspace.capabilityGuides, workspace.activatedCapabilityGuideDirs, inputPath);
|
|
589
|
+
if (capabilityGuideRead) {
|
|
590
|
+
return {
|
|
591
|
+
absolutePath: capabilityGuideRead.absolutePath,
|
|
592
|
+
readRoots: [workspace.root, capabilityGuideRead.guide.baseDir],
|
|
593
|
+
capabilityGuideRead,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
585
596
|
try {
|
|
586
597
|
return {
|
|
587
598
|
absolutePath: resolveAllowedPath(inputPath, workspace.root, [tmpdir()]),
|
|
@@ -597,6 +608,9 @@ export class WorkspaceRegistry {
|
|
|
597
608
|
if (readPath.skillRead?.isSkillFile) {
|
|
598
609
|
markSkillActivated(workspace.activatedSkillDirs, readPath.skillRead.skill);
|
|
599
610
|
}
|
|
611
|
+
if (readPath.capabilityGuideRead?.isGuideFile) {
|
|
612
|
+
markCapabilityGuideActivated(workspace.activatedCapabilityGuideDirs, readPath.capabilityGuideRead.guide);
|
|
613
|
+
}
|
|
600
614
|
}
|
|
601
615
|
resolveWorkingDirectory(workspace, workingDirectory) {
|
|
602
616
|
const directory = workingDirectory ? this.resolvePath(workspace, workingDirectory) : workspace.root;
|
|
@@ -631,8 +645,10 @@ export class WorkspaceRegistry {
|
|
|
631
645
|
sourceRoot: input.sourceRoot,
|
|
632
646
|
worktree: input.worktree,
|
|
633
647
|
...this.loadSkillsForWorkspace(input.root),
|
|
648
|
+
capabilityGuides: loadCapabilityGuides(this.config),
|
|
634
649
|
agentProfiles: await loadLocalAgentProfiles(this.config, input.root),
|
|
635
650
|
activatedSkillDirs: new Set(),
|
|
651
|
+
activatedCapabilityGuideDirs: new Set(),
|
|
636
652
|
};
|
|
637
653
|
this.store?.createSession({
|
|
638
654
|
id: workspace.id,
|
|
@@ -9,14 +9,14 @@ checkout.
|
|
|
9
9
|
`open_workspace` returns a `workspaceId`. Continue using that ID for later tools
|
|
10
10
|
in the same directory.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
`workspaceId` is a logical conversation handle, not the physical-directory
|
|
13
|
+
identity. Reopening the same checkout in the same conversation keeps that
|
|
14
|
+
logical ID stable. A different conversation normally receives a different
|
|
15
|
+
`workspaceId` even when it points at the same checkout or worktree; pass an
|
|
16
|
+
existing ID explicitly when the user wants to resume that logical workspace.
|
|
17
17
|
|
|
18
|
-
A Git worktree directory is a separate workspace
|
|
19
|
-
checkout.
|
|
18
|
+
A Git worktree directory is a separate physical workspace target from its source
|
|
19
|
+
checkout, and each conversation can still have its own logical handle for it.
|
|
20
20
|
|
|
21
21
|
## Checkout-first behavior
|
|
22
22
|
|
|
@@ -108,6 +108,37 @@ being injected eagerly. Read the relevant nested file before working under that
|
|
|
108
108
|
`FORGERELAY_AGENT_DIR` is not an instruction source; it remains only a compatibility
|
|
109
109
|
skill-discovery path.
|
|
110
110
|
|
|
111
|
+
## MCP capability loading
|
|
112
|
+
|
|
113
|
+
ForgeRelay keeps callable MCP tools and explanatory capability documentation
|
|
114
|
+
separate. `tools/list` remains the source of truth for what the current server
|
|
115
|
+
actually exposes; 0.3 does not hide callable tools behind documentation.
|
|
116
|
+
|
|
117
|
+
`open_workspace` adds two lightweight discovery surfaces:
|
|
118
|
+
|
|
119
|
+
- `capabilityFingerprint` is returned on every open/resume and includes the
|
|
120
|
+
ForgeRelay version, active tool mode, and stable semantic capability names;
|
|
121
|
+
- `capabilityGuides` is returned with bootstrap context and contains compact
|
|
122
|
+
descriptors for ForgeRelay-owned, versioned guides that can be loaded with
|
|
123
|
+
the normal `read` tool.
|
|
124
|
+
|
|
125
|
+
Do not preload every capability guide. Read a guide only when the current task
|
|
126
|
+
needs that domain. Built-in guides cover lifecycle Hooks, advanced managed
|
|
127
|
+
worktrees, subagents, artifact/change-review workflows, Host/OAuth/MCP App
|
|
128
|
+
integration, and long-running shell/PTY/process behavior. Optional guides are
|
|
129
|
+
advertised only when their feature is enabled; for example, disabled subagents
|
|
130
|
+
and artifact/change-review features do not add those descriptors to bootstrap
|
|
131
|
+
context. Reopening a workspace in the same Host context does not repeat the
|
|
132
|
+
descriptors, but the previously advertised guides remain valid.
|
|
133
|
+
|
|
134
|
+
The fingerprint is also a stale-Host-schema diagnostic. If `open_workspace`
|
|
135
|
+
reports a capability such as `filesystem.rename-move` but the Host's current
|
|
136
|
+
MCP tool snapshot does not expose `rename`, the server and Host metadata are out
|
|
137
|
+
of sync. Refresh/reconnect the MCP integration or start a Host context that
|
|
138
|
+
reloads `tools/list`; do not conclude that the running ForgeRelay server lacks
|
|
139
|
+
that capability. ForgeRelay can report its own version/capabilities but cannot
|
|
140
|
+
force the Host to discard a cached tool schema.
|
|
141
|
+
|
|
111
142
|
## Agent Skills
|
|
112
143
|
|
|
113
144
|
ForgeRelay discovers standard Agent Skills from:
|
|
@@ -134,7 +165,11 @@ config directory plus:
|
|
|
134
165
|
```
|
|
135
166
|
|
|
136
167
|
The workspace result exposes only compact profile metadata so the host can
|
|
137
|
-
choose a provider/profile without loading full provider launch details.
|
|
168
|
+
choose a provider/profile without loading full provider launch details. Read the
|
|
169
|
+
ForgeRelay-owned `subagents` capability guide when delegation is actually needed;
|
|
170
|
+
0.3 no longer auto-loads the historical bundled `subagent-delegation` Skill for
|
|
171
|
+
new setups. Existing user-authored or previously seeded Skills remain normal
|
|
172
|
+
user configuration and are not deleted.
|
|
138
173
|
|
|
139
174
|
The current model-facing delegation workflow is:
|
|
140
175
|
|
|
@@ -178,7 +213,8 @@ same workspace ID. The former process `sessionId` remains a deprecated alias in
|
|
|
178
213
|
|
|
179
214
|
Experimental `FORGERELAY_TOOL_MODE=codex` provides a smaller Codex-shaped
|
|
180
215
|
surface including direct `rename`/`delete` path mutations alongside `apply_patch`,
|
|
181
|
-
`exec_command`, and `write_stdin`.
|
|
216
|
+
`exec_command`, and `write_stdin`. `rename` is the unified move/rename primitive
|
|
217
|
+
for both files and directories; ForgeRelay does not expose a separate `move` tool.
|
|
182
218
|
|
|
183
219
|
Workspace IDs are logical conversation handles rather than physical-directory
|
|
184
220
|
identities. The same conversation keeps a stable ID for a project, while another
|
|
@@ -193,10 +229,16 @@ be released that way and must be finalized with `close_worktree`.
|
|
|
193
229
|
Shell commands are allowed to modify ordinary project files when that is a
|
|
194
230
|
natural part of the user's requested development task; ForgeRelay does not apply
|
|
195
231
|
a blanket ban to package managers, generators, formatters, or similar commands
|
|
196
|
-
that write files.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
232
|
+
that write files. They may also perform external device or hardware mutations
|
|
233
|
+
when the user's current request explicitly asks for the actual device-changing
|
|
234
|
+
operation. A check, audit, probe, backup, verification, dry-run, or build-only
|
|
235
|
+
request does not implicitly authorize a later persistent device write, and
|
|
236
|
+
ForgeRelay does not assume a particular flashing protocol or transport.
|
|
237
|
+
|
|
238
|
+
The Agent contract still prohibits shell mutation of security- or
|
|
239
|
+
privilege-sensitive operating-system files and credential material, and requires
|
|
240
|
+
an explicit user request before changing configuration files through `bash` or
|
|
241
|
+
`exec_command`.
|
|
200
242
|
|
|
201
243
|
## Change review UI
|
|
202
244
|
|
package/docs/configuration.md
CHANGED
|
@@ -123,6 +123,35 @@ MCP clients discover metadata from:
|
|
|
123
123
|
explicit tool mode is unset. The corresponding legacy `DEVSPACE_*` names are
|
|
124
124
|
also accepted.
|
|
125
125
|
|
|
126
|
+
The selected mode controls the real `tools/list` surface. ForgeRelay does not
|
|
127
|
+
hide callable tools behind capability documentation. In every mode,
|
|
128
|
+
`open_workspace` returns a `capabilityFingerprint` containing the package
|
|
129
|
+
version, tool mode, and stable semantic capability names. The fingerprint also
|
|
130
|
+
reports enabled optional domains such as subagent profile discovery, native
|
|
131
|
+
artifact download, MCP App UI, or aggregate `show_changes` review when those
|
|
132
|
+
features are actually available; it remains a semantic summary rather than a
|
|
133
|
+
copy of `tools/list`.
|
|
134
|
+
|
|
135
|
+
Bootstrap responses also return `capabilityGuides`, which are compact descriptors
|
|
136
|
+
for built-in ForgeRelay documentation that the Agent can explicitly load with
|
|
137
|
+
`read` when a task needs that domain. Current built-in domains cover lifecycle
|
|
138
|
+
Hooks, managed worktrees, subagents, artifact/change-review workflows, Host/OAuth/
|
|
139
|
+
MCP App integration, and long-running shell/PTY/process behavior. Optional-domain
|
|
140
|
+
descriptors such as `subagents` and `artifacts-review` are advertised only when
|
|
141
|
+
the corresponding feature is enabled, so disabled features do not add bootstrap
|
|
142
|
+
context.
|
|
143
|
+
|
|
144
|
+
There is no separate progressive-disclosure configuration switch. Capability
|
|
145
|
+
Guide discovery is built in, while actual tool exposure continues to be
|
|
146
|
+
controlled by `FORGERELAY_TOOL_MODE` and feature-specific settings. If the
|
|
147
|
+
fingerprint reports a capability that is missing from the Host's current tool
|
|
148
|
+
snapshot, treat that as stale Host MCP metadata: reconnect/refresh the integration
|
|
149
|
+
or use a Host context that reloads `tools/list`. The ForgeRelay process cannot
|
|
150
|
+
force a Host to invalidate its cached schema.
|
|
151
|
+
|
|
152
|
+
`rename` is the canonical move/rename primitive for files and directories; there
|
|
153
|
+
is no separate `move` MCP tool.
|
|
154
|
+
|
|
126
155
|
Codex-mode commands run without a PTY by default. `tty: true` enables interactive
|
|
127
156
|
programs when the optional `node-pty` dependency is available.
|
|
128
157
|
|
|
@@ -316,7 +345,8 @@ When subagents are enabled, profiles are discovered from:
|
|
|
316
345
|
- active legacy config directory `~/.devspace/agents/*.md` when reused;
|
|
317
346
|
- project `.devspace/agents/*.md` for migration compatibility.
|
|
318
347
|
|
|
319
|
-
The
|
|
348
|
+
The ForgeRelay-owned `subagents` capability guide teaches the current CLI
|
|
349
|
+
workflow on demand:
|
|
320
350
|
|
|
321
351
|
```bash
|
|
322
352
|
forgerelay agents ls
|
|
@@ -324,6 +354,11 @@ forgerelay agents run <profile-or-provider-or-id> "<prompt>"
|
|
|
324
354
|
forgerelay agents show <id>
|
|
325
355
|
```
|
|
326
356
|
|
|
357
|
+
0.3 no longer auto-discovers or seeds the package's historical bundled
|
|
358
|
+
`subagent-delegation` Skill for new setups. An existing or user-authored Skill
|
|
359
|
+
with that name remains an ordinary Skill and is still discovered from the normal
|
|
360
|
+
Skill paths when subagents are enabled; ForgeRelay does not delete or rewrite it.
|
|
361
|
+
|
|
327
362
|
## Logging
|
|
328
363
|
|
|
329
364
|
| Variable | Default |
|
|
@@ -334,7 +369,7 @@ forgerelay agents show <id>
|
|
|
334
369
|
| `FORGERELAY_LOG_ASSETS` | `0` |
|
|
335
370
|
| `FORGERELAY_LOG_TOOL_CALLS` | `1` |
|
|
336
371
|
| `FORGERELAY_LOG_SHELL_COMMANDS` | `1` in `pretty`, `0` in `json` |
|
|
337
|
-
| `FORGERELAY_TRUST_PROXY` | `0` |
|
|
372
|
+
| `FORGERELAY_TRUST_PROXY` | auto: `1` only for loopback bind + non-loopback public URL; otherwise `0` |
|
|
338
373
|
|
|
339
374
|
`pretty` is the human-facing local console format. It uses terminal-aware color,
|
|
340
375
|
short timestamps, workspace-first context, and compact operation results while
|
|
@@ -351,6 +386,14 @@ overridden, JSON mode preserves request logging and omits shell command previews
|
|
|
351
386
|
`FORGERELAY_LOG_REQUESTS` and `FORGERELAY_LOG_SHELL_COMMANDS` always override
|
|
352
387
|
these format-specific defaults when set.
|
|
353
388
|
|
|
389
|
+
When ForgeRelay binds to loopback (`127.0.0.1`, `::1`, or `localhost`) but is
|
|
390
|
+
configured with a non-loopback public URL, it automatically trusts exactly one
|
|
391
|
+
upstream proxy hop. This matches the normal tunnel/reverse-proxy topology and
|
|
392
|
+
keeps OAuth rate limiting aligned with Express client-IP resolution. Set
|
|
393
|
+
`FORGERELAY_TRUST_PROXY=0` to disable this inference, or `=1` to enable one-hop
|
|
394
|
+
trust explicitly. ForgeRelay never auto-enables proxy trust when binding to
|
|
395
|
+
`0.0.0.0` or another directly reachable interface.
|
|
396
|
+
|
|
354
397
|
## Environment-only example
|
|
355
398
|
|
|
356
399
|
```bash
|