agent-anywhere-gateway 0.1.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.
@@ -0,0 +1,175 @@
1
+ const MODE_OPTIONS = ["default", "auto-review", "full-access"];
2
+ const REASONING_EFFORT_OPTIONS = ["low", "medium", "high", "xhigh"];
3
+ const DEFAULT_REASONING_EFFORT = "high";
4
+ const DEFAULT_MODE = "auto-review";
5
+ const APPROVAL_POLICY_OPTIONS = ["never", "on-request"];
6
+ const MODEL_OPTIONS = ["gpt-5.5", "gpt-5.4", "gpt-5.3-codex", "gpt-5.2", "gpt-5-codex", "o3"];
7
+ const CAPABILITY_OPTION_LIMIT = 50;
8
+ const CAPABILITY_STRING_LIMIT = 128;
9
+
10
+ function uniqueOptions(values, fallback, { limit = CAPABILITY_OPTION_LIMIT, maxLength = CAPABILITY_STRING_LIMIT } = {}) {
11
+ const options = [];
12
+ for (const value of values || []) {
13
+ const option = String(value || "").trim();
14
+ if (option && option.length <= maxLength && !options.includes(option)) {
15
+ options.push(option);
16
+ if (options.length >= limit) {
17
+ break;
18
+ }
19
+ }
20
+ }
21
+ return options.length ? options : fallback;
22
+ }
23
+
24
+ function shortString(value) {
25
+ const text = String(value || "").trim();
26
+ return text && text.length <= CAPABILITY_STRING_LIMIT ? text : null;
27
+ }
28
+
29
+ function baseCapabilities(provider = "codex", overrides = {}) {
30
+ return {
31
+ providers: uniqueOptions(overrides.providers, [provider], { limit: 8 }),
32
+ models: uniqueOptions(overrides.models, MODEL_OPTIONS),
33
+ default_model: shortString(overrides.default_model),
34
+ input_modalities: uniqueOptions(overrides.input_modalities, []),
35
+ reasoning_efforts: uniqueOptions(overrides.reasoning_efforts, REASONING_EFFORT_OPTIONS),
36
+ approval_policies: uniqueOptions(overrides.approval_policies, APPROVAL_POLICY_OPTIONS),
37
+ modes: uniqueOptions(overrides.modes, MODE_OPTIONS)
38
+ };
39
+ }
40
+
41
+ function compactProviderCapabilities(provider = "codex", capabilities = {}) {
42
+ const compacted = baseCapabilities(provider, {
43
+ models: capabilities.models,
44
+ providers: [provider],
45
+ default_model: capabilities.default_model,
46
+ input_modalities: capabilities.input_modalities,
47
+ reasoning_efforts: capabilities.reasoning_efforts,
48
+ approval_policies: capabilities.approval_policies,
49
+ modes: capabilities.modes
50
+ });
51
+ compacted.providers = [provider];
52
+ if (compacted.default_model && !compacted.models.includes(compacted.default_model)) {
53
+ compacted.default_model = compacted.models[0] || null;
54
+ }
55
+ return compacted;
56
+ }
57
+
58
+ function compactProviderCapabilitiesMap(providerCapabilities = {}, providers = []) {
59
+ const map = {};
60
+ for (const provider of providers.slice(0, 8)) {
61
+ const key = shortString(provider);
62
+ if (!key || !providerCapabilities[key]) {
63
+ continue;
64
+ }
65
+ map[key] = compactProviderCapabilities(key, providerCapabilities[key]);
66
+ }
67
+ return map;
68
+ }
69
+
70
+ function buildCapabilities(provider = "codex", overrides = {}) {
71
+ const capabilities = baseCapabilities(provider, overrides);
72
+ if (capabilities.default_model && !capabilities.models.includes(capabilities.default_model)) {
73
+ capabilities.default_model = capabilities.models[0] || null;
74
+ }
75
+ const providerCapabilities = compactProviderCapabilitiesMap(overrides.provider_capabilities, capabilities.providers);
76
+ if (Object.keys(providerCapabilities).length) {
77
+ capabilities.provider_capabilities = providerCapabilities;
78
+ }
79
+ return capabilities;
80
+ }
81
+
82
+ function compactCapabilities(provider = "codex", capabilities = {}) {
83
+ return buildCapabilities(provider, {
84
+ models: capabilities.models,
85
+ providers: capabilities.providers,
86
+ default_model: capabilities.default_model,
87
+ input_modalities: capabilities.input_modalities,
88
+ reasoning_efforts: capabilities.reasoning_efforts,
89
+ approval_policies: capabilities.approval_policies,
90
+ modes: capabilities.modes,
91
+ provider_capabilities: capabilities.provider_capabilities
92
+ });
93
+ }
94
+
95
+ function capabilitiesForProvider(provider, capabilities = {}) {
96
+ const selected = String(provider || "").trim();
97
+ if (selected && capabilities.provider_capabilities?.[selected]) {
98
+ return capabilities.provider_capabilities[selected];
99
+ }
100
+ return capabilities;
101
+ }
102
+
103
+ function assertAllowedOption(name, value, options) {
104
+ if (!options.includes(value)) {
105
+ const error = new Error(`${name} 只能是:${options.join(", ")}`);
106
+ error.statusCode = 400;
107
+ throw error;
108
+ }
109
+ }
110
+
111
+ function optionList(capabilities, key, fallback) {
112
+ return uniqueOptions(capabilities?.[key], fallback);
113
+ }
114
+
115
+ function selectOption(input, defaults, key, options, fallback) {
116
+ const hasInput = input[key] !== undefined && input[key] !== null && String(input[key]).trim() !== "";
117
+ const raw = hasInput ? input[key] : defaults[key];
118
+ const fallbackValue = options.includes(fallback) ? fallback : options[0];
119
+ const value = String(raw || fallbackValue);
120
+ if (hasInput) {
121
+ assertAllowedOption(key, value, options);
122
+ return value;
123
+ }
124
+ return options.includes(value) ? value : fallbackValue;
125
+ }
126
+
127
+ function normalizeAgentSettings(input = {}, defaults = {}, capabilities = {}) {
128
+ const modelOptions = optionList(capabilities, "models", MODEL_OPTIONS);
129
+ const reasoningOptions = optionList(capabilities, "reasoning_efforts", REASONING_EFFORT_OPTIONS);
130
+ const approvalOptions = optionList(capabilities, "approval_policies", APPROVAL_POLICY_OPTIONS);
131
+ const modeOptions = optionList(capabilities, "modes", MODE_OPTIONS);
132
+
133
+ const model = selectOption(input, defaults, "model", modelOptions, capabilities.default_model || modelOptions[0]);
134
+ const reasoning_effort = selectOption(input, defaults, "reasoning_effort", reasoningOptions, DEFAULT_REASONING_EFFORT);
135
+ const approval_policy = selectOption(input, defaults, "approval_policy", approvalOptions, "on-request");
136
+ const mode = selectOption(input, defaults, "mode", modeOptions, DEFAULT_MODE);
137
+
138
+ return { model, reasoning_effort, approval_policy, mode };
139
+ }
140
+
141
+ function codexPolicyForMode(mode) {
142
+ if (mode === "auto-review") {
143
+ return {
144
+ sandbox: "workspace-write",
145
+ networkAccess: false,
146
+ approvalRisk: "low"
147
+ };
148
+ }
149
+ if (mode === "full-access") {
150
+ return {
151
+ sandbox: "danger-full-access",
152
+ networkAccess: true,
153
+ approvalRisk: "high"
154
+ };
155
+ }
156
+ return {
157
+ sandbox: "workspace-write",
158
+ networkAccess: false,
159
+ approvalRisk: "medium"
160
+ };
161
+ }
162
+
163
+ module.exports = {
164
+ APPROVAL_POLICY_OPTIONS,
165
+ DEFAULT_MODE,
166
+ DEFAULT_REASONING_EFFORT,
167
+ MODEL_OPTIONS,
168
+ MODE_OPTIONS,
169
+ REASONING_EFFORT_OPTIONS,
170
+ buildCapabilities,
171
+ capabilitiesForProvider,
172
+ compactCapabilities,
173
+ codexPolicyForMode,
174
+ normalizeAgentSettings
175
+ };
@@ -0,0 +1,26 @@
1
+ const GatewayMessageType = Object.freeze({
2
+ APPROVAL_DECISION: "approval_decision",
3
+ ERROR: "error",
4
+ HEARTBEAT: "heartbeat",
5
+ REQUEST: "request",
6
+ RESPONSE: "response",
7
+ RUN_REPLAY: "run_replay",
8
+ STREAM_COMPLETE: "stream_complete",
9
+ STREAM_ERROR: "stream_error",
10
+ STREAM_EVENT: "stream_event"
11
+ });
12
+
13
+ const GatewayRequestMethod = Object.freeze({
14
+ CANCEL_TURN: "cancel_turn",
15
+ DISCOVER_PROJECTS: "discover_projects",
16
+ LIST_RUNTIME_SESSIONS: "list_runtime_sessions",
17
+ READ_PROJECT_FILE: "read_project_file",
18
+ READ_RUNTIME_SESSION: "read_runtime_session",
19
+ RUN: "run",
20
+ STEER_TURN: "steer_turn"
21
+ });
22
+
23
+ module.exports = {
24
+ GatewayMessageType,
25
+ GatewayRequestMethod
26
+ };
@@ -0,0 +1,78 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+
4
+ const CONTENT_TYPES = {
5
+ ".html": "text/html; charset=utf-8",
6
+ ".css": "text/css; charset=utf-8",
7
+ ".js": "text/javascript; charset=utf-8",
8
+ ".json": "application/json; charset=utf-8",
9
+ ".svg": "image/svg+xml"
10
+ };
11
+
12
+ function sendJson(res, statusCode, payload) {
13
+ res.writeHead(statusCode, {
14
+ "Content-Type": "application/json; charset=utf-8",
15
+ "Cache-Control": "no-store"
16
+ });
17
+ res.end(JSON.stringify(payload));
18
+ }
19
+
20
+ function sendError(res, error) {
21
+ sendJson(res, error.statusCode || 500, {
22
+ error: error.message || "服务异常。"
23
+ });
24
+ }
25
+
26
+ async function readJson(req) {
27
+ let body = "";
28
+ for await (const chunk of req) {
29
+ body += chunk.toString("utf8");
30
+ if (body.length > 1024 * 1024) {
31
+ const error = new Error("请求体过大。");
32
+ error.statusCode = 413;
33
+ throw error;
34
+ }
35
+ }
36
+ if (!body.trim()) {
37
+ return {};
38
+ }
39
+ return JSON.parse(body);
40
+ }
41
+
42
+ function serveStatic(req, res, publicDir) {
43
+ const url = new URL(req.url, "http://localhost");
44
+ const safePath = path.normalize(decodeURIComponent(url.pathname)).replace(/^(\.\.[/\\])+/, "");
45
+ const targetPath = path.join(publicDir, safePath === "/" ? "index.html" : safePath);
46
+ const resolved = path.resolve(targetPath);
47
+ if (!resolved.startsWith(path.resolve(publicDir))) {
48
+ sendJson(res, 403, { error: "禁止访问该路径。" });
49
+ return true;
50
+ }
51
+ if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) {
52
+ return false;
53
+ }
54
+ res.writeHead(200, {
55
+ "Content-Type": CONTENT_TYPES[path.extname(resolved)] || "application/octet-stream",
56
+ "Cache-Control": "no-store"
57
+ });
58
+ fs.createReadStream(resolved).pipe(res);
59
+ return true;
60
+ }
61
+
62
+ function sseWrite(res, event) {
63
+ res.write(`data: ${JSON.stringify(event)}\n\n`);
64
+ }
65
+
66
+ function sseDone(res) {
67
+ res.write("data: [DONE]\n\n");
68
+ res.end();
69
+ }
70
+
71
+ module.exports = {
72
+ readJson,
73
+ sendError,
74
+ sendJson,
75
+ serveStatic,
76
+ sseDone,
77
+ sseWrite
78
+ };
@@ -0,0 +1,269 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+ const { randomUUID } = require("node:crypto");
4
+
5
+ const MAX_IMAGE_ATTACHMENTS = 8;
6
+ const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
7
+ const MAX_TOTAL_IMAGE_BYTES = 25 * 1024 * 1024;
8
+ const MAX_TEXT_ATTACHMENTS = 8;
9
+ const MAX_TEXT_BYTES = 512 * 1024;
10
+ const MAX_TOTAL_TEXT_BYTES = 2 * 1024 * 1024;
11
+ const IMAGE_EXTENSIONS = {
12
+ "image/gif": ".gif",
13
+ "image/jpeg": ".jpg",
14
+ "image/png": ".png",
15
+ "image/webp": ".webp"
16
+ };
17
+ const TEXT_MIME_TYPES = new Set([
18
+ "text/markdown",
19
+ "text/plain"
20
+ ]);
21
+
22
+ function sanitizeFileName(value, fallback = "image") {
23
+ const clean = String(value || "")
24
+ .replace(/[/\\?%*:|"<>]/g, "-")
25
+ .replace(/\s+/g, " ")
26
+ .trim()
27
+ .slice(0, 80);
28
+ return clean || fallback;
29
+ }
30
+
31
+ function parseImageDataUrl(dataUrl) {
32
+ const match = /^data:(image\/(?:png|jpeg|webp|gif));base64,([a-zA-Z0-9+/=\r\n]+)$/.exec(String(dataUrl || ""));
33
+ if (!match) {
34
+ const error = new Error("图片必须是 png、jpeg、webp 或 gif 的 base64 data URL。");
35
+ error.statusCode = 400;
36
+ throw error;
37
+ }
38
+ const buffer = Buffer.from(match[2].replace(/\s/g, ""), "base64");
39
+ return { mimeType: match[1], buffer };
40
+ }
41
+
42
+ function normalizeImageAttachments(input = []) {
43
+ if (!Array.isArray(input)) {
44
+ const error = new Error("图片附件格式不正确。");
45
+ error.statusCode = 400;
46
+ throw error;
47
+ }
48
+ if (input.length > MAX_IMAGE_ATTACHMENTS) {
49
+ const error = new Error(`一次最多上传 ${MAX_IMAGE_ATTACHMENTS} 张图片。`);
50
+ error.statusCode = 400;
51
+ throw error;
52
+ }
53
+
54
+ let totalBytes = 0;
55
+ return input.map((item, index) => {
56
+ const dataUrl = item?.data_url || item?.dataUrl;
57
+ const parsed = parseImageDataUrl(dataUrl);
58
+ const mimeType = String(item?.mime_type || item?.type || parsed.mimeType);
59
+ if (mimeType !== parsed.mimeType || !IMAGE_EXTENSIONS[mimeType]) {
60
+ const error = new Error("图片 MIME 类型不受支持。");
61
+ error.statusCode = 400;
62
+ throw error;
63
+ }
64
+ if (parsed.buffer.length > MAX_IMAGE_BYTES) {
65
+ const error = new Error("单张图片不能超过 10MB。");
66
+ error.statusCode = 400;
67
+ throw error;
68
+ }
69
+ totalBytes += parsed.buffer.length;
70
+ if (totalBytes > MAX_TOTAL_IMAGE_BYTES) {
71
+ const error = new Error("图片总大小不能超过 25MB。");
72
+ error.statusCode = 400;
73
+ throw error;
74
+ }
75
+ const id = String(item?.id || `image_${index + 1}`);
76
+ const name = sanitizeFileName(item?.name, `${id}${IMAGE_EXTENSIONS[mimeType]}`);
77
+ return {
78
+ id,
79
+ name,
80
+ mime_type: mimeType,
81
+ size: parsed.buffer.length,
82
+ data_url: `data:${mimeType};base64,${parsed.buffer.toString("base64")}`
83
+ };
84
+ });
85
+ }
86
+
87
+ function normalizeTextAttachment(item, index) {
88
+ const mimeType = String(item?.mime_type || item?.type || "text/plain").toLowerCase();
89
+ if (!TEXT_MIME_TYPES.has(mimeType)) {
90
+ const error = new Error("文本附件只支持 markdown 或 txt 文件。");
91
+ error.statusCode = 400;
92
+ throw error;
93
+ }
94
+ const content = String(item?.content || "");
95
+ const size = Buffer.byteLength(content, "utf8");
96
+ if (size > MAX_TEXT_BYTES) {
97
+ const error = new Error("单个文本附件不能超过 512KB。");
98
+ error.statusCode = 400;
99
+ throw error;
100
+ }
101
+ const id = String(item?.id || `text_${index + 1}`);
102
+ return {
103
+ id,
104
+ kind: "text",
105
+ name: sanitizeFileName(item?.name, `${id}${mimeType === "text/markdown" ? ".md" : ".txt"}`),
106
+ mime_type: mimeType,
107
+ size,
108
+ content
109
+ };
110
+ }
111
+
112
+ function normalizeAttachments(input = []) {
113
+ if (!Array.isArray(input)) {
114
+ const error = new Error("附件格式不正确。");
115
+ error.statusCode = 400;
116
+ throw error;
117
+ }
118
+
119
+ const images = [];
120
+ const texts = [];
121
+ for (const item of input) {
122
+ const mimeType = String(item?.mime_type || item?.type || "").toLowerCase();
123
+ if (mimeType.startsWith("image/") || item?.data_url || item?.dataUrl) {
124
+ images.push(item);
125
+ } else {
126
+ texts.push(item);
127
+ }
128
+ }
129
+ if (texts.length > MAX_TEXT_ATTACHMENTS) {
130
+ const error = new Error(`一次最多上传 ${MAX_TEXT_ATTACHMENTS} 个文本附件。`);
131
+ error.statusCode = 400;
132
+ throw error;
133
+ }
134
+
135
+ const normalizedTexts = texts.map(normalizeTextAttachment);
136
+ const totalTextBytes = normalizedTexts.reduce((total, item) => total + item.size, 0);
137
+ if (totalTextBytes > MAX_TOTAL_TEXT_BYTES) {
138
+ const error = new Error("文本附件总大小不能超过 2MB。");
139
+ error.statusCode = 400;
140
+ throw error;
141
+ }
142
+
143
+ return [
144
+ ...normalizeImageAttachments(images),
145
+ ...normalizedTexts
146
+ ];
147
+ }
148
+
149
+ function imageAttachmentsOnly(attachments = []) {
150
+ return attachments.filter((item) => String(item?.mime_type || item?.type || "").startsWith("image/"));
151
+ }
152
+
153
+ function materializeImageAttachments(attachments = [], { baseDir, sessionId = "session", turnId = "turn" } = {}) {
154
+ if (!attachments.length) {
155
+ return [];
156
+ }
157
+ const targetDir = path.join(
158
+ baseDir || path.join(process.cwd(), ".data"),
159
+ "attachments",
160
+ sanitizeFileName(sessionId),
161
+ sanitizeFileName(turnId)
162
+ );
163
+ fs.mkdirSync(targetDir, { recursive: true });
164
+
165
+ return attachments.map((item, index) => {
166
+ if (item.path) {
167
+ return item;
168
+ }
169
+ const { mimeType, buffer } = parseImageDataUrl(item.data_url);
170
+ const ext = IMAGE_EXTENSIONS[mimeType] || ".img";
171
+ const basename = sanitizeFileName(path.basename(item.name || `image-${index + 1}`, path.extname(item.name || "")));
172
+ const filePath = path.join(targetDir, `${String(index + 1).padStart(2, "0")}-${basename}-${randomUUID()}${ext}`);
173
+ fs.writeFileSync(filePath, buffer, { mode: 0o600 });
174
+ return {
175
+ id: item.id || null,
176
+ type: "local_image",
177
+ path: filePath,
178
+ name: item.name || path.basename(filePath),
179
+ mime_type: mimeType,
180
+ size: buffer.length
181
+ };
182
+ });
183
+ }
184
+
185
+ function storeImageAttachments(attachments = [], { baseDir, sessionId = "session", turnId = "turn" } = {}) {
186
+ return materializeImageAttachments(attachments, { baseDir, sessionId, turnId }).map((item) => ({
187
+ id: item.id || null,
188
+ kind: "image",
189
+ name: item.name || path.basename(item.path || "image"),
190
+ mime_type: item.mime_type || "image",
191
+ size: Number(item.size || 0),
192
+ storage: "control-file",
193
+ path: item.path
194
+ }));
195
+ }
196
+
197
+ function summarizeImageAttachments(attachments = []) {
198
+ return attachments.map((item) => ({
199
+ id: item.id || null,
200
+ name: item.name || path.basename(item.path || "image"),
201
+ mime_type: item.mime_type || item.type || "image",
202
+ size: Number(item.size || 0)
203
+ }));
204
+ }
205
+
206
+ function summarizeAttachments(attachments = []) {
207
+ return attachments.map((item) => ({
208
+ id: item.id || null,
209
+ kind: item.kind || (String(item.mime_type || item.type || "").startsWith("image/") ? "image" : "text"),
210
+ name: item.name || path.basename(item.path || "attachment"),
211
+ mime_type: item.mime_type || item.type || "application/octet-stream",
212
+ size: Number(item.size || 0),
213
+ data_url: item.data_url || undefined
214
+ }));
215
+ }
216
+
217
+ function promptWithImageSummary(message, attachments = []) {
218
+ const text = String(message || "").trim();
219
+ if (!attachments.length) {
220
+ return text;
221
+ }
222
+ const summary = `[已附加 ${attachments.length} 张图片]`;
223
+ return text ? `${text}\n\n${summary}` : `图片输入\n\n${summary}`;
224
+ }
225
+
226
+ function promptWithAttachmentContent(message, attachments = []) {
227
+ const text = String(message || "").trim();
228
+ const images = imageAttachmentsOnly(attachments);
229
+ const textAttachments = attachments.filter((item) => item?.kind === "text");
230
+ const parts = [];
231
+ if (text) {
232
+ parts.push(text);
233
+ }
234
+ if (images.length) {
235
+ parts.push(`[已附加 ${images.length} 张图片]`);
236
+ }
237
+ for (const attachment of textAttachments) {
238
+ parts.push([
239
+ `--- 文本附件:${attachment.name || "untitled"} (${attachment.mime_type || "text/plain"}) ---`,
240
+ attachment.content || "",
241
+ "--- 文本附件结束 ---"
242
+ ].join("\n"));
243
+ }
244
+ if (!parts.length) {
245
+ return "附件输入";
246
+ }
247
+ if (!text && images.length && parts.length === 1) {
248
+ return `图片输入\n\n${parts[0]}`;
249
+ }
250
+ return parts.join("\n\n");
251
+ }
252
+
253
+ module.exports = {
254
+ MAX_IMAGE_ATTACHMENTS,
255
+ MAX_IMAGE_BYTES,
256
+ MAX_TOTAL_IMAGE_BYTES,
257
+ MAX_TEXT_ATTACHMENTS,
258
+ MAX_TEXT_BYTES,
259
+ MAX_TOTAL_TEXT_BYTES,
260
+ imageAttachmentsOnly,
261
+ materializeImageAttachments,
262
+ normalizeAttachments,
263
+ normalizeImageAttachments,
264
+ promptWithAttachmentContent,
265
+ promptWithImageSummary,
266
+ storeImageAttachments,
267
+ summarizeAttachments,
268
+ summarizeImageAttachments
269
+ };
@@ -0,0 +1,110 @@
1
+ const fs = require("node:fs");
2
+ const os = require("node:os");
3
+ const path = require("node:path");
4
+
5
+ function defaultAllowedRoots() {
6
+ return [path.dirname(process.cwd())];
7
+ }
8
+
9
+ function parseAllowedRoots(raw) {
10
+ const roots = raw
11
+ ? raw.split(",").map((item) => item.trim()).filter(Boolean)
12
+ : defaultAllowedRoots();
13
+ return roots.map((root) => path.resolve(root));
14
+ }
15
+
16
+ function isInside(childPath, rootPath) {
17
+ const relative = path.relative(rootPath, childPath);
18
+ return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative));
19
+ }
20
+
21
+ function expandHome(rawPath) {
22
+ return rawPath.startsWith("~") ? path.join(os.homedir(), rawPath.slice(1)) : rawPath;
23
+ }
24
+
25
+ function nearestExistingAncestor(resolvedPath) {
26
+ let current = resolvedPath;
27
+ while (!fs.existsSync(current)) {
28
+ const parent = path.dirname(current);
29
+ if (parent === current) {
30
+ return current;
31
+ }
32
+ current = parent;
33
+ }
34
+ return current;
35
+ }
36
+
37
+ function realpathForPolicy(resolvedPath) {
38
+ const existingAncestor = nearestExistingAncestor(resolvedPath);
39
+ const realAncestor = fs.realpathSync.native(existingAncestor);
40
+ const remainder = path.relative(existingAncestor, resolvedPath);
41
+ return remainder ? path.resolve(realAncestor, remainder) : realAncestor;
42
+ }
43
+
44
+ function realAllowedRoots(allowedRoots) {
45
+ return allowedRoots.map((root) => {
46
+ const resolved = path.resolve(expandHome(root));
47
+ return fs.existsSync(resolved) ? fs.realpathSync.native(resolved) : resolved;
48
+ });
49
+ }
50
+
51
+ function rejectOutside(resolved) {
52
+ const error = new Error(`项目路径不在允许根目录内:${resolved}`);
53
+ error.statusCode = 400;
54
+ throw error;
55
+ }
56
+
57
+ function resolveProjectPath(rawPath, allowedRoots = parseAllowedRoots(process.env.AGENT_ANYWHERE_ALLOWED_ROOTS)) {
58
+ if (!rawPath || typeof rawPath !== "string") {
59
+ const error = new Error("项目路径不能为空。");
60
+ error.statusCode = 400;
61
+ throw error;
62
+ }
63
+
64
+ const expanded = expandHome(rawPath);
65
+ const resolved = path.resolve(expanded);
66
+ const allowed = allowedRoots.some((root) => isInside(resolved, root));
67
+
68
+ if (!allowed) {
69
+ rejectOutside(resolved);
70
+ }
71
+
72
+ const realResolved = realpathForPolicy(resolved);
73
+ const realAllowed = realAllowedRoots(allowedRoots).some((root) => isInside(realResolved, root));
74
+
75
+ if (!realAllowed) {
76
+ rejectOutside(resolved);
77
+ }
78
+
79
+ return resolved;
80
+ }
81
+
82
+ function openOrCreateProjectPath(rawPath, { create = false, allowedRoots } = {}) {
83
+ const projectPath = resolveProjectPath(rawPath, allowedRoots);
84
+ const exists = fs.existsSync(projectPath);
85
+
86
+ if (create) {
87
+ if (exists && !fs.statSync(projectPath).isDirectory()) {
88
+ const error = new Error("目标路径已存在但不是目录。");
89
+ error.statusCode = 400;
90
+ throw error;
91
+ }
92
+ fs.mkdirSync(projectPath, { recursive: true });
93
+ return projectPath;
94
+ }
95
+
96
+ if (!exists || !fs.statSync(projectPath).isDirectory()) {
97
+ const error = new Error("项目目录不存在。");
98
+ error.statusCode = 404;
99
+ throw error;
100
+ }
101
+
102
+ return projectPath;
103
+ }
104
+
105
+ module.exports = {
106
+ isInside,
107
+ openOrCreateProjectPath,
108
+ parseAllowedRoots,
109
+ resolveProjectPath
110
+ };